Unwrap Or Throw (or Die)
Swift’s Optional
type is one of the biggest hits of the language. Mainly because it’s opt-in. Everything is non-optional by default, which solves the old problem of having to deal with nulls everywhere. That said, nulls (or Optional.none) still shows up a decent amount of times, so the second thing Swift did very well was to provide great affordances to work with optional at the language level.
You should read this Twitter thread from Jordan Rose about the decisions of making Optional so convenient.
One of the common operations that you want to do with an optional is to coalesce the nil with a default value. For that, Swift provides the handy nil-coalescing operator ??
. This operator is so nice that we will use it to improve on the ergonomics of optional later.
But the reality is that there is still a bunch of behaviour that Swift doesn’t make so convenient. For example, another part of the language that is convenient to use is Errors. And the language provides a mechanism to silence errors, to convert a throwing error to an optional. That’s what try?
does. But there is no convenient way of going the other way around!
Another important aspect that people often complain about is how force unwrapping !
has a very convenient syntax but if leads to opaque logs and crashes with very little information.
The community quickly realised all of this in the early days of Swift. The concern about the lack of proper diagnostics for force unwrapping was put in the table by Erica Sadun on 2017 (see the pitch) and it even had a proposed solution (SE-0217: Introducing the !!
“Unwrap or Die” operator to the Swift Standard Library) that was eventually rejected. Even tho it was rejected the core team agreed it was something worth discussing and that improvements on the language (Never being a true bototm type) could offer alternative better solutions.
I was never interested in that operator since I barely want to “die” when working with optionals. And when I do !
is good enough for me. But what I want often is to throw an error if the optional is nil. Conversations about this also started a while back, Unwrap or Throw - Make the Safe Choice Easier and Introducing an “Unwrap or Throw” operator.
If you read all those conversations, you see that there are multiple ways of solving the same problem and everybody has different opinions and needs.
What I want to share today results from my gathering of the solutions mentioned over the years and taking the ones I like the most. I’ve been using this code in my projects for years, so this is maybe not how Swift will solve it eventually, but it’s the way I prefer to solve it.
My Desired solution
What I want is to be able to use the existing tools in the language in all the ways I need. That is, use the nil-coalescing operator??
but instead of giving a default value, throw an error or even crash.
That translates to code that should look like:
try optional ?? throw(Error())
optional ?? fatalError("fancy message")
I don’t think new operators are needed. The ??
already provides the semantic definition that we need: unwrap the optional on the left and if it’s nil use whatever there is on the right. Usually the right side will have a default value, but it doesn’t have to be always like that!
Coalescing with raise
The problem is that if we try to use throw
on the right side, we will find a compiler error. That’s because Swift’s throw
is not an expression, so we can’t use it in this way.
We can make a very simple function that throws the given error, call it raise
. It’s a very simple function, but it makes it possible to throw errors in places where an expression is needed.
try someWork() ?? raise(YourError())
Just with this function, you can get very close to the ideal syntax of an “unwrap or throw” operator. Some will even prefer this to the next alternatives, since it doesn’t introduce any new operator overloads into the language. This is the solution with less impact.
Coalescing with ??
But if you want to have a more succinct syntax, you can overload the coalescing operator. With it, there is no need for the raise
function.
try someWork() ?? YourError()
I think that’s all I wanted! ^^
Method unwrapOrThrow
That said, some people don’t love overloading operators or even abusing them in this way. For those, it’s quite easy to just make an extension in Optional that gives this functionality a proper name: unwrapOrThrow
.
try someWork().unwrapOrThrow(YourError())
You can even make a default error UnwrapError
. With that, you can call the method without specifying a custom error.
try someWork().unwrapOrThrow()
☠️ Unwrap or die
As I said, the “unwrap or die” is not something I ever used. But with the setup we have so far is quite easy to retrofit this functionality. All the same techniques that we use for throwing can apply to fatalError
.
Why fatal error? The common complain about !
is that it doesn’t give a reason when it crashes. It’s like a fatalError()
. But if we use fatalError("reason for crashing")
we can attach a reason explaining why we thought the optional should have had some value and never nil.
We can start by just using the raise
function.
try someWork() ?? raise(fatalError("reason for crashing"))
This works but the compiler will show a warning on this line: Will never be executed
. That’s because the compiler detects that the raise function itself will never be executed.
Instead, we can overload ??
to accept a Never
on the right side.
try someWork() ?? fatalError("reason for crashing")
Of course, if you prefer to use the method, it’s even easier since it just works.
try someWork().unwrapOrThrow(fatalError("reason for crashing"))
Unwrap or reason?
A common request is to have a way to unwrap or pass a String directly for the reason of the crash, instead of using a fatalError(_:)
. I barely use the unwrap or die technique, so I don’t have the need for further convenience.
If you want to do that, then you start needed other operators and functions since otherwise you make things ambiguous when the optional contains a String. Is the right String a reason to crash or just a safe default?
I will leave this as an exercise to the reader.
Wrapping it all together
After all this analysis, and years of using some of these solutions, I decided to open source a small Swift package with these conveniences: UnwrapOrThrow.
Unwrap Or Throw (or Die!): 🎁 Unwrap an optional or throw an error if nil (or crash the program).
There are other solutions out there, but I liked none of them, so this is just yet another one. But is the one I like ^^.