Unwrap or throw is a scenario in which we want to throw an error if an optional returns a nil value. Techniques like if let
or guard statements make this easy to do but often return in quite some boilerplate code.
In cases like this, I’m always hoping to find a solution I wasn’t aware of. Even better: I’m seeking a Swift Evolution proposal for a new Swift feature that I didn’t know yet. In this case, I ended up in this forum thread exploring several code solutions that I’ll shine a light on in this article. The forum thread refers to SE-0217 – The “Unwrap or Die” operator, which got rejected. Without going too much into detail about that rejection, I started exploring solutions we have today that can simplify our code in cases we want to throw errors after finding a nil value.
Unwrap or throw without fancy extensions
Before diving into fancy solutions for throwing errors after finding a nil value, it’s good to understand how to write such a code solution today.
Imagine having a structure defining upload input for an Uploader that takes a JSON Web Token (JWT) as input. A JWT can contain several claims which we need to decode to find our upload input values. Whenever a value doesn’t exist, we want to throw a detailed error. The code would look as follows:
struct UploadInput {
enum UploadInputError: Error {
case invalidJWT
case missingIdentifier
case missingUploadURL
}
let uploadIdentifier: String
let uploadURL: URL
init(token: JWT) throws {
guard let decodedToken = JWTDecode.decode(jwt: token) else {
throw UploadInputError.invalidJWT
}
guard let uploadIdentifier = decodedToken.claim(name: "upload.id").string else {
throw UploadInputError.missingIdentifier
}
guard let uploadURL = decodedToken.claim(name: "upload.url").url else {
throw UploadInputError.missingUploadURL
}
self.uploadIdentifier = uploadIdentifier
self.uploadURL = uploadURL
}
}
The initializer of the above code example is readable and understandable. However, if we were to add more unwraps with detailed errors, we could easily end up with a large initializer that becomes less readable.
It would be great to improve the above code example by directly assigning the unwrapped value to the instance properties or, otherwise, throwing an error immediately. Let’s explore how we can solve this by implementing a solution for unwrap or throw.
Exploring solutions for throwing an error on nil
Swift enables us to write several solutions to unwrap or throw an error if a nil value is found. As mentioned before, the following code examples are inspired by this Swift forum thread that can be an inspirational place if you want to explore this topic a bit more.
In the following code solutions, we will zoom into the initializer of the above code example and show the final result of using the given optimization.
Using a closure
The first solution already shows how we can minimize the code in our initializer by writing a smarter solution around throwing an error when a nil value is found:
init(token: JWT) throws {
let decodedToken = try JWTDecode.decode(jwt: token) ?? { throw UploadInputError.invalidJWT }()
uploadIdentifier = try decodedToken.claim(name: "upload.id").string ?? { throw UploadInputError.missingIdentifier }()
uploadURL = try decodedToken.claim(name: "upload.url").url ?? { throw UploadInputError.missingUploadURL }()
}
However, using a closure here does not improve readability.
Using a method to throw an error
By writing a generic method, we can replace the above closure to make it a bit more readable:
func throwError<T>(_ error: Error) throws -> T {
throw error
}
init(token: JWT) throws {
let decodedToken = try JWTDecode.decode(jwt: token) ?? throwError(UploadInputError.invalidJWT)
uploadIdentifier = try decodedToken.claim(name: "upload.id").string ?? throwError(UploadInputError.missingIdentifier)
uploadURL = try decodedToken.claim(name: "upload.url").url ?? throwError(UploadInputError.missingUploadURL)
}
This code example improves readability over the closure example and is a good candidate to replace our original initializer. However, in my journey to find the best solution, I hoped to find a custom operator instead to unwrap or throw. The original rejected proposal included such an operator, so let’s see if we can replicate this today.
Using a custom nil coalescing operator
Not everybody is a fan of using custom operators in Swift. They can be compact and convenient but are much harder to find in a new codebase. This is a quote from the rejected proposal:
Adding operators, in particular operators without term of art precedent, in general has a high bar. Operators, while compact and convenient to use once learned, are harder to learn, being more difficult to search for, talk about in speech, or query in IDEs than named functions.Joe Groff
It’s up to you to decide whether you like operators or not in this case. To share with you my opinion: I love using them! Yet, I also have to agree with my colleagues before implementing this in our projects.
Let’s dive into the code example and explore how a custom operator could improve our initializer to unwrap or throw. First, we have to define the custom nil coalescing operator:
infix operator ?!: NilCoalescingPrecedence
/// Throws the right hand side error if the left hand side optional is `nil`.
func ?!<T>(value: T?, error: @autoclosure () -> Error) throws -> T {
guard let value = value else {
throw error()
}
return value
}
The operator method uses an autoclosure combined with generics. Using it in our initializer results in the smallest outcome so far:
init(operator token: JWT) throws {
let decodedToken = try JWTDecode.decode(jwt: token) ?! UploadInputError.invalidJWT
uploadIdentifier = try decodedToken.claim(name: "upload.id").string ?! UploadInputError.missingIdentifier
uploadURL = try decodedToken.claim(name: "upload.url").url ?! UploadInputError.missingUploadURL
}
Personally, I prefer this solution the most. It comes with the downside of learning about the new ?!
operator but results in the most compact yet readable code, in my opinion.
Conclusion
Exploring code solutions in Swift is fun and makes you a better engineer. You’ll find solutions written by others that will inspire you to improve your code. Even if you don’t end up using any of the other found solutions, you’ve still learned a lot about unwrapping or throw code solutions available in Swift today.
If you like to learn more tips on Swift, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.
Thanks!