Todayâs article will be about handling errors in Swift.
Because letâs be honest, that makes a fun post title for the season âď¸âď¸.
Objective-C and the NSError of its ways
Remember Objective-C? Back then1, the main and official way for a method to tell that something went wrong was to take a NSError*
by reference.
NSError* error;
BOOL ok = [string writeToFile:path
atomically:YES
encoding:NSUTF8StringEncoding
error:&error];
if (!ok) {
NSLog(@"An error happened: %@", error);
}
Man that was painful. To the point that many people were tempted to not even bother to check for errors and simply pass NULL
there. Not very responsible and safe.
Do you want to build a throw, man?
With Swift 2.0, Apple decided to introduce a different way to handle errors: using throw
2.
The use of throw
is actually quite simple:
- If you want to create a function that might fail, mark it with
throws
in its signature; - Inside the function itself you can use
throw someError
if needed; - At call site, youâll have to explicitly use
try
in front of your calls to methods which can throw3; - To catch errors and handle them, use
do { ⌠} catch { ⌠}
constructs.
This looks like this:
// Define a function that can throwâŚ
func someFunctionWhichCanFail(param: Int) throws -> String {
...
if (param > 0) {
return "somestring"
}
else {
throw NSError(domain: "MyDomain", code: 500, userInfo: nil)
}
}
// ⌠then call it
do {
let result: String = try someFunctionWhichCanFail(-2)
print("success! \(result)")
}
catch {
print("Oops: \(error)")
}
The failure never bothered me anyway
You can see that someFunctionWhichCanFail
returns a plain String
, which is the type returned when everything was ok. It makes it easy to call the function ânormallyâ, first thinking (in the do { ⌠}
block) about the happy path, to handle the case where nothing wrong happens.
The only reminder that those methods can fail is the try
keyword that the compiler enforces you to add in front of that call, otherwise itâs like a non-throwing function call. And then, you only write the code to handle errors in a separate place (inside the catch
)
Note that you can write more than one line (and try
-call more than one throwing function) in that do
block. If everything is successful, it will execute them as expected, but as soon as one of them fails it will jump out of the do
block into the catch
statement instead. Thatâs very handy as well for long runs of code with multiple potential points of failure, as you can handle them all in a single error path at the end.
NSError is a bit of a fixer-upper
Ok, but with this example we still have to handle errors using NSError
, which is a pain. Comparing domains and error codes with ==
and make a list of domain and code constants, just to know which error we got and handle it properly⌠ouch.
But we can fix this fixer-upper⌠up with a little bit of love! What if we used what we learned in my Enums as Constants article and use enums
to represent errors instead?
Well, Good news, everyone!â˘, thatâs exactly what Apple intended in this new error handling model!
In fact, when a function throws
, it can throw any object which conforms to ErrorType
. NSError
is one of those types, but you can make your own, and itâs even recommended!
The best fit for an ErrorType
type is an enum
, which can even have associated values if needs be. For example:
enum KristoffError : ErrorType {
case ClumsyWayHeWalks
case GrumpyWayHeTalks
case PearShapedSquareShapedWeirdnessOfHisFeet
case NotWashedSince(days: Int)
}
Then you can now use throw KristoffError.NotWashedSince(days: 3)
in a function to throws this kind of error, then use catch KristoffError.NotWashedSince(let days)
at call site to catch such errors:
func loveKristoff() throws -> Void {
guard daysSinceLastShower == 0 else {
throw KristoffError.NotWashedSince(days: daysSinceLastShower)
}
...
}
do {
try loveKristoff()
}
catch KristoffError.NotWashedSince(let days) {
print("Ewww, he hasn't had a shower since \(days) days!")
}
catch {
// Any other kind of error, whatever it is
print("I prefer we stay friends")
}
Thatâs way easier to catch specific errors than having to compare domains and error codes!
That also makes better errors with clear names as constants and associated values. No more obscure userInfo
, you have a clear list of associated values right there in the enum case of the error â like days
in the above example â which are only valid for that specific type (wouldnât make sense to have that days
associated value for the ClumsyWayHeWalks
error).
I canât hold it back anymore
When you call a throw
-ing function, the error it throws can be caught in the calling function using a doâŚcatch
. But if it isnât, then it propagates to the upper level:
func doFail() throws -> Void { throw ⌠}
func test() {
do {
try doTheActualCall()
} catch {
print("Oops")
}
}
func doTheActualCall() throws {
try doFail()
}
Here when doFail
is called, the potential error is not caught by doTheActualCall
(there is no doâŚcatch
capturing it), so it propagates up to the calling test()
function. Because it doesnât catch all errors, doTheActualCall
must also be marked as throws
: even if it doesnât throw errors by itself, it can still propagate some. It canât keep the error to itself, it has to⌠let it go to the upper level.
On the other end, test()
catches all errors internally so even if it calls a throwing function (try doTheActualCall()
), all errors thrown by that function are caught in the doâŚcatch
block. The test()
function itself doesnât throw, so callers donât need to know about this internal behavior.
Conceal, donât feel, donât let them know
You may have wondered by now how to know which kind of error each method throws. Indeed, functions are marked with throws
but what ErrorType
can this function actually throw? Can it throw KristoffErrors
, JSONErrors
, other? Which ones do I need to catch?
Well thatâs a problem. Currently, due to some ABI and resilience concerns4, this is not possible. The only way to know is using the documentation of your code.
But thatâs also a good thing. For example, imagine you use two libraries, MyLibA
containing a function funcA
that throws
errors of type MyLibAError
, and MyLibB
with a function funcB
that throws
errors of type MyLibBError
.
Then you want to create your own library MyLibC
, a wrapper around those two libraries, with a function funcC()
calling both MyLibA.funcA()
and MyLibB.funcB()
. So the resulting function funcC
might throw either errors of type MyLibAError
or MyLibBError
. And if you add another level of abstraction, it gets worse, with more and more error types being able to be thrown. If we had to list them all, and the call site would need to catch them all, that would make a quite verbose signature and catch
code.
Donât let them in, donât let them see
For that reason, but also to prevent your internal errors from bleeding across your library boundaries and to limit the number of error types that must be handled by your users, I suggest that you keep your error types scoped to each level of abstraction.
In the example above, instead of making funcC
directly propagate both MyLibAErrors
and MyLibBErrors
, you should instead throw MyLibCErrors
. I suggest this for two reasons, both related to abstraction:
- Your users shouldnât have to know which internal library youâre using. If some day in the future you decide to switch your implementation to use
SomeOtherPopularLibA
instead ofMyLibA
, which will obviously not throw exactly the same errors, the caller of your ownMyLibC
framework shouldnât need to know or care. Thatâs what abstraction is all about. - The caller shouldnât have to handle all the errors. Surely you can catch some of those errors and consider them internal: not all errors thrown by
MyLibA
make sense to be exposed publicly to your users, for example aFrameworkConfigurationError
error mentioning that you misused theMyLibA
framework and forgot to call itssetup()
method or whatever should not make its way to the user, as the user canât do much about it. That kind of error is your fault, not theirs.
So instead, your funcC
should probably catch all MyLibAErrors
and MyLibBErrors
, wrap them / re-interpret them and expose them as MyLibCErrors
instead. That way, the users of your framework donât have to know what youâre using under the hood. You can switch your internal implementation and libs used at any time, and you expose to the user only the errors they might care for.
We finish each others sandwiches5
There is a lot more to tell about throw
and the Swift 2.0 error model. I could talk about try?
and try!
, about the rethrows
keyword for high-order functions.
I wonât have time to talk about every subject about error handling there, that would have made that post way too long; but other interesting blog posts might help you finish your exploration in the world of Swift error handling, including (but not limited to) those:
- Throw that donât throw and ReâŚthrows? by Rob Napier
- Error Handling by Little Bites of Cocoa
- What we learned from rewriting our robotic control software in Swift, by Brad Larson
- ⌠(Donât hesitate to share more links in the comments section!)
Let me finish this article by wishing you all happy holidays âď¸âď¸ and see you soon for the next post!
-
For more info about how the old way to handle errors in Objective-C work, see this great article by NSHipster. But todayâs article is about Swift and the new way, so letâs not lose too much time about that old beast. ↩
-
Despite its name,
throw
is not about throwing exceptions like in Java or C++ or even ObjC. But the way to use it is so similar that Apple decided to keep the same wording so that people used to exceptions find it natural. ↩ -
This is enforced by the compiler, the aim being to let you realize that this function can go wrong and you have to handle the potential error ↩
-
Swift 2.0 doesnât support typed throws, but there is a discussion about adding that feature in the swift-evolution Mailing List where Chris Lattner explains why this was not possible in Swift 2 and why we need the resilience model of Swift 3.0 to be able to make that feature consistent. ↩
-
Ok, promise, that was my last shameful Frozen reference. ↩