When Swift was first announced, I was gratified to see that one of the (few) philosophies that it shared with Objective-C was that exceptions should not be used for control flow, only for highlighting fatal programming errors at development time.
So it came as a surprise to me when Swift 2 brought (What appeared to be) traditional exception handling to the language.
Similarly surprised were the functional Swift programmers, who had put their faith in the Haskell-style approach to error handling, where every function returns an enum (or monad, if you like) containing either a valid result or an error. This seemed like a natural fit for Swift, so why did Apple instead opt for a solution originally designed for clumsy imperative languages?
I'm going to cover three things in this post:
-
Why the Swift designers are saying that Swift's new errors are not really exceptions, and why that is both true and false.
-
Why the result enum approach to error handling is not a universal solution for error handling in Swift.
-
What I would have done instead.
- The Exception That Proves the Rule
Why would Apple tell us that exceptions are a bad way to handle errors, and then introduce exceptions as the way to handle errors in Swift? Is this just another example of the reality distortion field that brought us "7-inch tablets are too small", "5-inch phones are too big", "Dynamic dispatch rules, vtables drool", etc?
Yes and no. Exceptions are/were deemed as bad for two reasons, one technical and one usability-related:
The technical reason is that the stack unwinding triggered by an exception either causes memory leaks due to skipped release statements, or imposes runtime performance costs and binary bloat if the necessary code to prevent these leaks is generated by the compiler for every method.
The usability reason is that it is not apparent when calling a function whether it (or or one of the functions it calls internally) may throw an exception, and so it is hard to reason about whether you've handled all possible error scenarios, short of wrapping every call to third party code in a try/catch (this also ties into the performance problem - if we knew which methods could and could not potentially throw exceptions, the code could be optimized).
But the latter of these two problems was actually solved long ago in Java by the use of checked exceptions, which require that every method that can throw an exception declares this fact in its interface, as a sort of alternative return value.
And Swift's exceptions are exactly that: they are just syntax sugar around a new kind of return statement. There's no expensive and complicated stack unwinding as the program jumps out of its normal flow, skipping stack frames until it reaches the nearest catch statement; Swift's solution adds no more overhead than the old NSError
pointer arguments in Objective-C, and in fact seems to piggy back on the same mechanism (or is at least functionally compatible).
But, implementation aside, Swift's errors have almost the exact same syntax and semantics as Java's checked exceptions (bar some tinkering with the way try
is used) and are exactly as cumbersome to use, but without the performance costs that Java inherited from its earlier, unchecked behavior.
So "don't use exceptions for flow control" was not some kind of general statement of good programming practice - it was purely a matter of technical convenience. They're telling us that Objective-C's exceptions are inefficient, and you shouldn't use them, but Swift's are OK.
And are they OK? Well, we'll get to that.
- The Functional Imperative
Repeat after me: "Swift is not Haskell!"
Swift's methods have side effects. Swift's objects have mutable state. Singletons are alive and well in Swift. Swift doesn't come with lazy-by-default evaluation, or monads, or signals, or promises, or the "await" keyword, or Haskell-esque "do" statements. Writing pure functional code in Swift is awkward and impractical because none of the system APIs you need to use to actually get shit done have any interest in your functional aspirations.
As a community, we embraced result enums in Swift because they're a lot nicer to use than the old Objective-C approach of passing a pointer to an error and checking for nil or false results. But let's be honest - a good part of the reason for that is that Swift makes the old pattern considerably more awkward to use than it was in Objective-C, thanks to Optional
unwrapping, stricter type checking, and obfuscation of pointers. Passing a null pointer into a function and checking if it's still null when it's finished is just not the Swift way to do things.
I'm not saying that result enums are not objectively better than error params. Just that the reason people like them in Swift has as much to do with convenience as it does functional purity or ensuring code correctness.
Brad Larson wrote a good article about why he liked result enums, and why he's switching to the new Swift 2 error system anyway, and the answer is basically because it lets him do the same thing with less boilerplate.
When push comes to shove, convenience trumps functional purity for most programmers, and I think that's perfectly reasonable.
But
If you look at the examples in that article, the reason why the result enum doesn't work well for him is that the methods being called are primarily synchronous, and without return values. They aren't pure functions. This is imperative logic, not functional logic. Using return values either means checking the value after every call, or forces multiple consecutive lines of imperative logic to be turned into a chain of closures passed to then
methods, which feels weird and restrictive when the lines themselves are independent of one another in every respect except execution order.
If this was Haskell, we'd use the do
keyword to let us write our functional-code-that's-really-imperative-code as if it was just regular imperative code (via the power of monads and a bit of syntax sugar), but that's a PitA to do in Swift because the syntax support isn't there and you'd have to build the monadic infrastructure yourself (people have tried this of course - needless to say, many custom operators are involved).
But does that mean result types are bad? No, they're pretty awesome for functions that actually return a result that we want to pass to the next function (possibly an unspecified amount of time later). They just aren't great for handling errors produced by procedures that don't return anything, and execute sequentially on the same thread, because they must be checked after every call, and that quickly becomes tedious.
The same is true of Objective-C's error pointers (which is why so many developers just pass NULL and forget about them). The mechanism by which the error is returned isn't that important - it's the mechanism by which it is handled on the calling side that matters, and that's the thing that Swift's exceptions improve on - by replacing endless, tedious if (error != nil) { return error }
statements with try
.
But try
only works in this one situation, with sequential imperative statements. If Brad Larson was working on something like a network library instead of a robot controller, result types would work much better for error propagation than Swift 2's exceptions, because exception-style errors don't really work for asynchronous, callback-based APIs. Consider the following example:
enum Result<T, U> {
case Success(T)
case Failure(U)
}
sendRequest(url: NSURL, callback: { result: Result in
switch result {
case let .Success(result): // handle success
case let .Failure(error): // handle error
}
}
Internally, sendRequest may be doing something like
sendRequest(url: NSURL, callback: callback) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
var result, error;
// do something that takes a long time and either yields a result or an error
if (result) {
dispatch_async(dispatch_get_main_queue()) {
callback(.Success(result))
}
} else {
dispatch_async(dispatch_get_main_queue()) {
callback(.Failure(error))
}
}
}
}
How would this work using exceptions? If we tried to throw inside our dispatch block we'd be stuck because it would be GCD that receives the error, not our calling function (which has already exited by this point), nor our callback function (which will never be called). There's nowhere we can usefully put our try
or catch
.
And wrapping up these callbacks in a promise-based API makes no difference either; If our sendRequest
function returns a promise instead of accepting a callback, we still need the errors to be handled internally by sending the error as an argument to a block/closure, not returning it from a function where it has been thrown. We can't return an exception instead of a promise, because at the point of return we don't yet know if the request has succeeded or not.
Result types and traditional Objective-C errors can be used either as return values or as callback arguments, which makes them suitable for both synchronous and asynchronous logic. Exceptions are just a special type of return value, and although they could be passed as a function argument, the try/catch semantics for using them can only be applied if they are thrown (aka returned) from a function, not passed to one.
- A Solution Tailored for Swift
So if Swift's not-really-new not-really-exceptions work nicely for synchronous, imperative code, and result enums work well for pure functions and async callbacks or promises, we're all good, right? The system works.
Nope.
It's gross that we have two totally different ways to handle errors based on whether the following line needs to use the output from the previous line, and whether it's called immediately, or after an async calculation. And Swift’s try/catch semantics do nothing to help reduce the error handling boilerplate needed inside callbacks.
The basic problem that exceptions (including Swift's) solve is that they change the default behavior of a function return. Normally, if a function returns, execution moves to the next line. But when the return value is an exception, we want execution to skip to the next catch statement, or to return the exception from the calling function so it can be handled by the caller's caller instead (and so on).
We could just make a rule that if a function returns an ErrorType
, and the caller doesn't do something with it, we return from the function instead of continuing execution. I quite like this solution, as it avoids the need for throw, throws, do, try and catch, but it may be a little bit too magical.
So maybe we keep the try
keyword, but change the meaning to "if this code returns anything other than an ErrorType
, do nothing, otherwise bump us out of the current function and return the error". That would look like this:
func doSomethingThatMayFail() -> ErrorType? {
...
}
func doSomeThings() -> ErrorType? {
try doSomethingThatMayFail() // if function returns nil, continue, otherwise return error
doTheNextThing()
}
The doSomethingThatMayFail
function doesn't return anything normally, but will return an error if it fails, as indicated by the ErrorType? return value. This is equivalent to writing func doSomethingThatMayFail() throws
in Swift 2, but without needing a new keyword.
There would be two ways to call doSomethingThatMayFail() - we could either explicitly store its return value, or we could use try
. Using try
would be functionally equivalent to this:
func doSomeThings() -> ErrorType? {
if let error = doSomethingThatMayFail() {
return error
}
doTheNextThing()
}
Ignoring the result completely would be a compile-time error (or maybe just a warning, so it doesn't slow down prototyping). Using try
inside a function that doesn't return an ErrorType
would also be a compile-time error/warning.
There's probably no need for a catch
keyword - if we want to handle the error, we just skip the try
and handle the error value explicitly. Otherwise, we let it bubble up until we get to a function that does need to handle it.
What about functions that return a value on success? How would we use those with try
? Well, these would need to return an enum of the form:
enum Result<T: Any, U: ErrorType> {
case Success(T):
case Failure(U)
}
And try
would then decompose this into just the success value if the function didn't return an error, like this:
let foo = try functionThatReturnsFooOrAnError()
Doing that decomposition in the general case would be tricky, as it would involve slicing the error condition from an enum to form a new type. But if Result
was a standard construct, like Optional
, it would be much simpler. Instead of an arbitrary enum of "ErrorType + whatever", functions would have to return either a Result
or an ErrorType?
to be used with try
.
(Aside: I'm abusing the ? modifier here to mean "or void" instead of "or nil", but the more I think about it, those are really the same thing. In an ideal version of Swift, I don't see a need for nil and void to be distinct concepts at all. A benefit of this would be that functions that return ErrorType?
wouldn't have to explicitly return nil
on success, they'd just use naked return
, or reach the closing }
without returning, implying success.)
So what about asynchronous callbacks - does this new definition of try
help us there? Or is it no better than Swift 2's existing exception system (other than requiring fewer keywords)?
Well, if we examine how we're using try
to decompose Result
into ErrorType
and a legitimate value, we can see a parallel with how if let
or guard let
is used to decompose Optional
s.
Just as guard let x = x
takes an Optional
and either gives us a non-nil value or breaks out of the execution context, try
does the same thing for Result
. So for a callback, we could use the same kind of logic:
sendRequest(url: NSURL, callback: { result: Result in
let value = try result // decomposes Result into value/error, returns if error
// do something with result
}
This approach would require that our callback has a return type of ErrorType?
instead of void, but it means we can use the same shortcut in our async callback as we can in our synchronous functions - bail at the first error and notify the caller that we've done so.
Since the semantics of try
are similar to guard
, we could even support an optional else
, so that it mirrors the guard
syntax:
let value = try result else {
// clean up
return result // at this point result is known to be an error
}
In which case we avoid the requirement that we must return an ErrorType?
from our callback. We could instead forward the error to another function, for example:
let value = try result else {
callErrorHandler(result)
return
}
So, with a single try
keyword, coupled to the established result enum pattern, we can support the same error handling approach introduced in Swift 2, with the same level of clarity, but with less boilerplate, and we can apply the same solution to async callback-based functions or promises.
In this post I've addressed why I believe Swift 2 introduced exceptions instead of result enums as its core error handling mechanism, and proposed an alternative that I think would better serve both its functional and imperative use cases.
Of course, I don't anticipate that the Swift language designers will read this and do away with the throw
, throws
and catch
keywords. The purpose of this is mostly a thought experiment into how a hybrid functional/imperative language such as Swift might handle errors elegantly.
But hopefully some budding language designer out there may find some food for thought here when pondering the error mechanism for their new language.
Your proposed solution is exactly how error handling is done in Rust: https://doc.rust-lang.org/stable/book/error-handling.html