Handling non-optional optionals in Swift
Optionals are arguably one of the most important features of Swift, and key to what sets it apart from languages like Objective-C. By being forced to handle the case where something could be
nil, we tend to write more predictable and less error prone code.
However, sometimes optionals can put you in somewhat of a tight spot, where you as the programmer know (or at least, you’re under the assumption) that a certain variable will always be non-
nil when used, even if it’s of an optional type. Like when handling views in a view controller:
This is where Swift programmers will disagree to almost the same level as with tabs vs spaces. Some say:
“Since it’s an optional, you should always properly unwrap it, using either if let or guard let.”
While others will go in the completely different direction and say:
“Since you know the variable will not be nil, force unwrap it (using !). Crashing is better than ending up in an undefined state.”
Basically what we’re talking about here, is whether or not to do defensive programming. Do we try to recover from an undefined state, or do we simply give up and crash?
If I had to give a binary answer to that question — I’d most definitely go with the latter. Undefined states lead to really hard to track down bugs, may lead to unwanted code execution and employing defensive programming will just lead to code that is hard to reason about.
But, I’d prefer not to have to give a binary answer, and instead look into some techniques that we can use to work around this problem in a much more nuanced way. Let’s dive in!
Is it really optional?
Variables and properties that are optionals, but are actually required by the program logic, are actually a symptom of an architectural flaw. If something is needed, to the point where not having it puts you in an undefined state — it shouldn’t be optional.
While there are cases (such as when interacting with certain system APIs) where optionals are really hard to avoid — there are some techniques we can use to get rid of optionals in many situations.
Being lazy is better than being non-optionally optional
One way of avoiding optionals for properties which values need to be created after the parent object is created (such as views in a view controller — which should be created in
viewDidLoad()) is through the use of lazy properties. A lazy property can be non-optional, while still not being required to be created in its parent’s initializer. It will simply be created when first accessed.
Let’s update our
TableViewController from before to use a lazy property for its
No optionals, no undefined state! 🎉
Proper dependency management is better than non-optional optionals
Another common use of optionals is to break circular dependencies. You can sometimes get into situations where
A depends on
B also depends on
A. Like in this setup:
As we can see above, we have a circular dependency between
CommentManager, where neither of them assumes ownership over the other, but they still rely on each other for part of their logic. That is just bugs waiting to happen! 😅
To solve the above problem, we’re instead going to make
CommentComposer act as a middleman, and take on the responsibility of notifying both
CommentManager that a comment has been made:
UserManager can hold a strong reference to
CommentManager, without any retain (or dependency) cycles:
We have again removed all optionals and have predictable code! 🎉
Above we saw a couple of examples where we could tweak our code to remove uncertainty by removing optionals. However, sometimes that’s not possible. Let’s say you are loading a local JSON file containing the configuration for your app. This is inherently an operation that can fail, so we’re going to need to add some error handling.
Continuing program execution in case the config fails to load will put the app in an undefined state, so in this case it’s better to crash. That way we get a crash report, and hopefully our tests & QA process will catch this problem long before it reaches our users.
So, how do we crash? The simplest solution is to simply use the
! operator, to force-unwrap the optional, causing a crash if it contains
While this approach is simple, it comes with a pretty big downside. If this code starts crashing, all we get as an error message is:
fatal error: unexpectedly found nil while unwrapping an Optional value
The error message doesn’t tell us why and where the error occurred, and gives us no clues on how to fix it. Instead, let’s use the
guard statement, combined with the
preconditionFailure() function, to exit with a custom message.
When crashing using the above, we will get a much more helpful error message:
fatal error: Configuration couldn’t be loaded. Verify that Config.JSON is valid.: file /Users/John/AmazingApp/Sources/AppDelegate.swift, line 17
We now have a clear action we can take to fix the problem, and we know exactly where in our codebase it occurs! 🚀
Doing the above
guard-let-preconditionFailure dance can be a bit tedious, and it does make the code a bit harder to follow. We really don’t want to give extraordinary circumstances like this so much room in our code — we want to focus on our logic.
My solution to that is Require. It adds a simple
require() method on
Optional that does the above, but leaves the call site a lot more clean. Here’s what the above configuration loading code looks like when using Require:
Which will give us the following error message if it fails:
fatal error: Required value was nil. Debugging hint: Verify that Config.JSON is valid: file /Users/John/AmazingApp/Sources/AppDelegate.swift, line 17
Another advantage of Require is that it will also throw an
NSException as well as calling
preconditionFailure, which will enable crash reporting tools like Crashlytics to pick up all the metadata of the crash.
So to summarize, these are my tips for handling non-optional optionals in Swift:
- Being lazy is better than being non-optionally optional
- Proper dependency management is better than non-optional optionals
- Crash gracefully when you need to use non-optional optionals
Thanks for reading! 🚀