Swift Delight: try
Swift delight: `try`
— Jordan Rose (@UINT_MIN) December 21, 2021
I don’t mean the whole error handling model; there are definitely pros and cons there. I just want to focus specifically on `try`. That said, I do have to talk about the model a bit to establish what Swift does differently.
Part of the Swift Regrets series.
Swift’s whole error handling model definitely has pros and cons, but I just want to focus specifically on try
. That said, I do have to talk about the model a bit to establish what Swift does differently.
There are essentially two classes of error handling implementation: caller-checked, and “zero-cost exceptions”. In the caller-checked model, after a function returns, the caller checks something to see if there was an error—the return value, a special out-parameter, something. By contrast, in the “zero-cost exceptions” model a function doesn’t return if there’s an error. Instead it jumps to some cleanup code and eventually to an error handler, manually resetting the stack as needed. (There are a few ways to implement this, some more “zero-cost” than others.) It’s “zero-cost” because if you don’t have an error there’s nothing to check, no extra code run, but if there is an error it usually ends up being more work / code size than it would have been to just check at the call site. So it’s a trade-off.
These are the two main implementation models. The user model in turn can look like manual checking or like automatic propagation, and that’s why we’re talking about all this. Swift’s error handling user model is automatic propagation but it’s implemented with manual checking.
(…Technically I could have skipped everything up to that point, but y’all like learning, right? So to learn a lot more about error handling impls and user models, John McCall did an excellent survey and write-up as part of designing Swift’s error model.)
Anyway, Swift’s error handling model looks a lot like C++’s or Java’s or C#’s, with throw
and catch
keywords. But in C++ or Java or C#, you can’t tell what statements might throw errors. Which means you have to be very careful about which order you do operations in, lest one of them throw an error and leave your object, or something else, in an inconsistent state. The classic example is a mutex left locked, but more subtle would be something like a partially-written output file.
Swift’s contribution to the space is moving try
from “something in this block might throw” to “something in this statement might throw”. (The block marker goes from try
/catch
to do
/catch
, and do
/while
goes to repeat
/while
.) Like Optional replacing null
, the real win is in the lines that don’t have try
: now you know they can’t throw. And all other “exiting” control flow in a function is marked: break
, continue
, throw
, and return
. try
might be conditional, but that doesn’t mean it should be completely unmarked.
This doesn’t solve all the “inconsistent state” / “cleanup” problems; you still have to think about that at all the try
lines. But it’s certainly a step forward!
I don’t think Swift’s try
is perfect. To avoid a deluge of required keywords on one line, it applies to everything to its right, rather than just the operation immediately after it. That is, in try foo(bar())
, bar()
is permitted to throw as well. I also think there’s room to improve around calling rethrows
functions with single-expression closures, where you end up with a try
outside and inside. And over the years several people have argued that it’s just noise in certain circumstances, though I don’t always agree. So it might still evolve in the future, or in the next language.
But overall I think marking the control flow that is propagating errors is something that makes the auto-propagating user model more usable all around, in a way that makes it easier to not make mistakes.
P.S. What about the manual checking model? I think the equivalent feature is locally opting in to propagation, like Rust’s ?
operator. let foo = bar()?;
in Rust is almost exactly the same as let foo = try bar()
in Swift, despite the differing user model elsewhere.