Often I need to transform a type to another, such as a networking model to a data model, or a data model to a binary representation. Should these transformation functions take an Optional/nullable value and immediately return nil if it's nil, or should the function only accept non-nil values and always return a non-nil transformation? Assume my code properly handles validation when receiving data, so no further validation is needed, just transformation.

Example 1: Have the caller handle nil objects

func makeUser(from apiUser: APIUser) -> User {
    return User(id: apiUser.id)

var user: User? = nil
func downloadUser() {
    service.getUser { (apiUser: APIUser?) in
        if let apiUser = apiUser {
            self.user = makeUser(from: apiUser)
        } else {
            self.user = nil

Example 2: Have the transform handle nil objects

func makeUser(from apiUser: APIUser?) -> User? {
    if apiUser == nil {
        return nil
    return User(id: apiUser.id)

var user: User? = nil
func downloadUser() {
    service.getUser { (apiUser: APIUser?) in
        self.user = makeUser(from: apiUser)

Example 1 explicitly states an APIUser will always transform to a User object. While Example 2 implicitly implies that a valid APIUser may not map to a valid User object, even though that is guaranteed.

While Example 2 does demonstrate that the common logic is handled in the transform instead of copied each time it's needed, I personally don't like the fact that the transform can return an optional/nullable value even though my function contract states validation has already been performed and a valid value will never fail.

4 Answers 4


If the only purpose of an optional parameter is to return nil if the parameter is nil then I wouldn’t do it. Get some additional type safety by making the parameter non-optional. Then anyone having a possibly nil value can check for that. Quite often it turns out the value is actually never nil and there is no checking code in the end.

  • Since asking this question, I do write my code without accepting nil just to return nil. Going over my codebase, the vast majority of functions do that, and it results in endless optional checks throughout. Mainly, just because we didn't check it when it was inserted into the db long ago. Commented Apr 3, 2020 at 21:20

The makeUser function takes a value and creates a user out of it. If it cannot create a user because the input is invalid, it should throw an exception. Null is definitely an invalid input as you can't parse "nothing" and get a User.

This is similar to parsing an int. If the input is invalid, throw an exception, don't pretend it went well and then return the default value.

If you do this, then whenever you call makeUser you know that you definitely have an actual User object, not null.

downloadUser should then validate that the user it downloaded is valid (in your case that seems to just be checking that it isn't null).

While it doesn't particularly matter for this one use case, the benefit is that other calls to makeUser either work (in which case you have a user object) or have to explicitly be handled by the calling code. This helps reduce the chances of null reference exceptions throughout the app.

EDIT: For summary, Example 1 is better to me.

  • In Swift you don’t throw exceptions all that often. Returning nil is quite appropriate. Whether the argument should be optional at all, that is the question.
    – gnasher729
    Commented Oct 22, 2019 at 11:29
  • In swift parsing an String to Int will not throw, it will return nil. developer.apple.com/documentation/swift/int/2927504-init. I prefer to only use throw for exception handling and not for control flow. I do agree that I rather my function have non-optional, just I rather not throw. Commented Oct 22, 2019 at 15:43
  • You wouldn't be throwing to control flow. You would be throwing to tell the calling code "You need to pass valid input, or you need to not call me". That forces the calling code to handle this using an if statement (like you had in your example) rather than leaving it to chance. When the app is running, no exception will, as the calling code will check input first (like in your example). This is a defensive exception for future calling code to let the developer know what they should and should not pass to the method. Regardless, I don't know enough about swift, so sorry for that!
    – Jack
    Commented Oct 22, 2019 at 16:42
  • @Jack From the example, it's possible and legal for the value from the network layer to be nil. I believe, if the caller changes it's control flow based on the catch then it's control flow and not exception handling. Here, I'm interested in where to handle the nil check. In the function or outside of the function. Commented Oct 22, 2019 at 18:28
  • I agree that it is legal for the network layer to be nil. I was suggesting that this is not a legal value to be nil in makeUser. I didn't suggest a catch statement, so i'm a little unsure where that assumption has come from. makeUser can throw an unhandled exception to force the developer to put the if statement in. As I said, i'm unsure on best practices in swift, I'm basing this anwser (and comments) entirely on trying to avoid the use of null's as much as possible, and in my eyes a function called makeUser should never return null (as if it returned null, it didnt make a user.)
    – Jack
    Commented Oct 23, 2019 at 10:34

The APIUser, being a DTO, exists at the boundary of your application. It should not be leaking out beyond the object that interacts with the remote service. Neither example is the right solution. The real problem lies with your service not returning a User object and performing the mapping from APIUser to User internally.

As for nullability? It's a remote system. You do not control it. It can be null.

Your service should be refactored to return a User?. If the API returns a null value, the service should return a nil. You need to give your application a chance to handle situations where the user does not exist in the remote service. This might be an innocent mistake where someone tried to log in, but they don't have a user profile yet. No need to crash the application when a polite "You do not have permission to access this application" message would suffice.

So basically the answer is Example 3 (below)

// 'service' calls API and maps to User domain object
var user: User? = service.getUser()

if user == nil {
    // user not found in remote service, handle error

This is honestly no different than querying a database for your application. The user might not exist. Return a nil instead of crashing the application with an exception you probably need the user to handle anyhow.

  • I don't think you understood the example. In both cases I show that the service does in fact return an optional User object. The question is how to handle the optional object after the service returned it. Either way, you didn't address the question. Which is whether the transform function should handle nil or the optional should be handled by the caller. Commented Oct 22, 2019 at 15:35
  • @MichaelOzeryansky: There shouldn't be a transform function outside of the service. That was the main point of my answer. The service should return a User? and the mapping of APIUser to User should be a concern for the service, not the code using the service. Commented Oct 22, 2019 at 15:38
  • APIUser is a network object, and the User is a data model. These exist within separate layers of the framework. Do you recommend having both the APIUser and the User object defined in the network layer? If the server returned malformed data, I would normally handle that with a Result type returning .failure, not a nil User. Commented Oct 22, 2019 at 15:39
  • @MichaelOzeryansky: Can you include more context about where this "makeUser" function is defined and where it is being called? I feel like there is an architectural issue to resolve here. Commented Oct 22, 2019 at 15:50
  • Regardless of where it is, I asked this question to address whether transformer functions should take a nil value. The network example is just an example. There are many other places where I need to transform a types, such as going from Model to ViewModel. Commented Oct 22, 2019 at 18:25

Callers might want to handle nil differently

The power of Optional is that it gives users to handle nil in many different ways.

  1. They can substitute a default value using the nil-coalescence operator (??)
  2. They can assert that it's not nil, using the force unwrap operator (!)
  3. They can transform the value, using map or flatMap

There's no universally correct approach. So any determination you make within your callee-side handling is likely to be wrong for at least some callers. That's why we should let callers handle nil themselves.

It would make a total mess

Thinking about this practically, if it was decided that "callees should handle nil for callers", than all Swift code would be pervasively littered with optionals. That's not very great.

There's already a tool for this job

What you would find is a lot of repetition of the same code that you just wrote "check if x is nil, and if it's not, transform it by passing it through f, otherwise just pass the nil along quietly". When we identify repetition in code like this, we should aim to extract it out using some kind of abstraction. In this case, it already exists, in the form of Optional.map(_:).

Given these two functions:

func makeUserHandlingOptionals(from apiUser: APIUser?) -> User? { ... }
func makeUser(from apiUser: APIUser) -> User { ... }

One can be trivially implemented in terms of the other:

func makeUserHandlingOptionals(from apiUser: APIUser?) -> User? {
   return apiUser.map(makeUser(from:))

It's so trivial, in fact, that you shouldn't declare makeUserHandlingOptionals(from:) at all. If a caller wants to transform a APIUser?, just let them call map themselves.

A similar story in Java

In the past, I've had a similar temptation occur to me. I was writing Java before 1.8 (which introduced the stream APIs). I had a function that took an A and returned a B. But I had an ArrayList<A>, and it was very tedious to convert it to an ArrayList<B>. I had to do it myself every time:

B convertAtoB(A a) { ... }

ArrayList<A> inputOfAs = ...
ArrayList<B> outputOfBs = new ArrayList();

for (A inputA : inputOfAs) {

I needed to do that a lot, and that was annoying. I've had a similar temptation as you did, perhaps I should introduce a second overload version of convertAtoB, which took an ArrayList<A> and returned an ArrayList<B>. But then that seems annoying, so maybe the right choice is to only have the array version, and pass it single-element arraylists when I need to transform a single element.

I didn't have a good solution, until I eventually learned about streams and "functional style" constructs like map. map completely solves this problem, by abstracting away the details of making an (A) -> B function able to work as if it were a ([A]) -> [B] function. That way, convertAtoB can stay focused on converting A to B, and not fiddle around with array lists.

Switching to map even improved performance. At the time, I didn't realize that repeaded ArrayList.add calls would cause occasional resizing operations. When the current array got full, ArrayList would have to allocate a new larger one, and spend O(n) time copying the old elements over. Only to do so again momentarily later. The implementer of map would know that the input and output arrays always have the same size (by the definition of the map). Thus they could pre-allocate an output array that's precisely large enough to fit all the new B elements, without resizing and without excess. That's something I hadn't thought about at the time, and got "for free" in the process.


TL;DR: Your function should stay focused on doing the transformations it's responsible for.

  • Very short and to the point. Thanks. Commented Apr 3, 2020 at 21:15
  • @MichaelOzeryansky Without being able to read tone over text, that sounds really sarcastic :p
    – Alexander
    Commented May 1, 2020 at 21:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.