Back in the Objective-C days, any object variable could be
nil. If you sent a message to
nil pointer, the method basically turned into a no-op (returning
nil again, so nested
Objective-C calls like
[[MYClass alloc] init] would just collapse into a no-op if the
first call failed).
This seemed a convenient way to do error handling: Just return
nil on error and whoever
calls you doesn’t have to handle errors at every step of the way. However, it was also
so very implicit, that often programs encountered errors and just … stopped reacting.
Some variable had ended up being set to
nil, and the app just kept calling into the void,
where nothing happened.
Swift learned from this, and made nullability an explicit attribute of a variable’s type.
If a variable could become
nil, the API contract now said so. If you tried to use a
nullable value without checking that it wasn’t
nil beforehand, that was a compiler error.
But since Swift was used on top of the Cocoa frameworks, which were written in Objective-C,
many properties were marked as nullable that actually could never become
nil. But since
the Objective-C code didn’t mark up nullability, how was Swift to know? So nullability was
retrofitted into Objective-C, and the system headers marked up, just enough so Swift could
know which properties needed to be optional and which didn’t.
Still, I see a lot of Swift code that basically contains “extra line noise” in the form
nil checks that “should never happen”. Code that is basically replicating Objective-C’s
nil-collapsing behaviour in a more verbose way. There is no recovery from these cases.
Often they are
guard statements that simply abort execution of the current method.
This is good: It is explicit. As soon as you read the line, you see that the function will abort in this degenerate error case.
This is bad: A lot of code that is never run, or is only run in a debug build when you make a mistake editing some code, clutters up the lines of a method, making it harder to follow the actual intent of the method.
Furthermore, methods that return a value often need to “lie” about their result upon such a failure. If they don’t, they in turn become failable (and their return types optional):
If a method they call can fail in the degenerate case, the method around it can fail in that same case. At least in Swift this is now explicit in the code, but it also means that we’ve basically arrived back at procedural C’s error handling, which cluttered up our algorithms, when the reaction to each failure usually ended up being the equivalent of “I’m sorry, Dave. I’m afraid I can’t do that.”
This is why throwable errors (like C++ exceptions and Swift errors) were introduced: To
move the (usually identical) error handling out-of-band and reduce the amount of code
dealing with error checking in favor of the code actually handling the errors.
if block is hidden behind that tiny
I don’t know why exactly, but throwable errors seem to have a bad reputation among Swift
programmers. Admittedly, if you look at C++ exceptions, it is very easy to overlook who
may throw. But Swift has the
try label on failable lines, so can that really be the reason?
Is it a holdover from Objective-C and Swift 1.0, where exceptions were only used for programming errors and system code was simply not prepared to handle them for mostly historical reasons? Is it a dislike for the added exception mark-up that is less subtle than making a variable’s type optional and adds deeper nesting?
Is it maybe an awareness of the fact that, once everything can fail, it becomes hard again to tell what actually will fail, and when, and we’re back where C++ is, where nobody will really deal with failures that “shouldn’t happen”? But isn’t that where we already are with all those optional checks?
I think current Swift code still suffers from being architected like it was Objective-C when
it comes to optionals, but while the cognitive overhead of optionals (
nil messaging) was
comparatively low beyond reading a line and doing a quick “what happens if this goes
optionals in Swift cost us time blanking their associated checks out so we can
“see the algorithm between them.”
This is odd, given Swift is at its core a language that prefers a high density of operations per statement, and makes a lot implicit (like variable types, through inference).
Whether you like the number of operations Swift crams into a single statement, that is
definitely what the language was designed for, and I think we should try to strive for
this level of focus in our algorithms as well. And that to me includes designing them to
avoid optionals (and their associated error checks) where it furthers clarity.
Make it invalid to even start running a method or algorithm before we’ve checked that
our parameters aren’t
nil. This up-fronts the error checks and chances of failure,
instead of littering them all through our code.
Similarly, this should be possible with callbacks. Design our methods so they have separate
result and completion callbacks. If there are no results, we then don’t have to give a
in a completion block that they have to check for, but rather just don’t call the result callback
at all. Alternately, if we’re dealing with collections, we can give empty arrays
instead of a
nil array. Then anyone iterating over the list just doesn’t do anything.
In its own way, these two techniques are like Objective-C’s
nil-collapsing again. Of course,
that means it is similarly implicit. What if the loop contains a call that must be done,
even for 0 items? Then we would again have to add a conditional for the “0 items” case
that covers it. Might as well let the compiler remind us of that, then, and make it an optional.
But where we can front-load these checks, our algorithms can become a lot clearer, simpler, at the back end of the equation. Fewer optional checks. More raw control flow dealing with the core of our functionality.
Optionals as a code smell
Given most projects have existing code that isn’t front-loaded yet, and we’re writing
against operating system frameworks that were designed to take advantage of Objective-C’s
nil-checks is not something we can just do in a week of
But I think all it takes to improve our Swift code is to raise awareness of this issue, to make Swift programmers pause each time before they declare an optional and think:
“An optional! Ewww! Can I somehow make this non-optional or front-load it?”
If optionals are treated like any other code smell, I think our Swift code will put our actual application logic back in the spotlight, and thus help us detect the complex bugs instead of hiding them among lines of “this should never happen” guard statements.