Stop fighting with Swift's frustrating do-catch blocks. Catch errors simply.
Swift's error handling is powerful, safe and well-designed. But do catch blocks have meaningful flaws which leads to complex code reasoning, false sense of security, and unhandled errors.
To fully understand the problem you should read my deep dives here:
I'll summarize the main points here:
doblocks with multipletrystatements are problematic because:- When the function errors and jumps to the
catchblock, it is not clear whichtrystatement caused the error. - (Most of the time) the error is untyped, so you have to dynamically cast it to the correct type before you can read and handle it.
- The Trapped Scope Problem The result of the
trystatement is not available outside of thedoblock, so in practice you end up putting more work in thedoblock (which only exacerbates the problem). - Thrown errors abruptly exit the
doblock, which creates multiple code paths you have to consider.
- When the function errors and jumps to the
- Swift actually has TWO type systems:
- The type system for the function signature including the return type.
- The type system for the thrown error.
[!WARNING] Not API Stable This library is currently in beta development and is not API stable. The API may change in the future without notice.
This library provides various tool to handle errors in a more ergonomic way through the following strategies:
Any function marked throws effectively has two return types:
- The return type of the function
- The error type
This library provides convenient functions to convert the two return types into a single return type such as Optional, Result, or a default value.
catch blocks do not tell you which try statement caused the error. This library provides a way to handle errors in place without the need for a do catch block.
To convert throwing functions, the library not only accepts throwing closures, but also applies the @autoclosure attribute. This is mainly for one reason: the @autoclosure forces us to send in one and only one throwing function. This means that we know exactly what triggered our catch block.
This library operates under the philosophy each throwing function should be handled in one of two ways:
- Call the
tryfunction within athrowscontext so that the error can "bubble up" and be handled by the appropriate caller. - Handle the error in place, immediately before proceeding so that we know exactly what caused the error.
Swift 6 introduced typed errors, which allows us to define the error type in the function signature. This library embraces this new feature and attempts to have feature parity between typed and untyped errors.
This library fully supports async await and provides the same functionality for async functions.
This library provides safer strategies to run throwing functions without:
- creating a
throwscontext - creating a
docatchblock
Catch and handle your errors simply and ergonomically by:
- replacing the error with an
Optional - replacing the error with a reasonable value
- bundling the error and value in a
Result - simply handling the error in place
The library comes with a new initializer for Optional which allows you to provide a closure
to read and handle the error. Then it's easy to unwrap it, just like any other Optional.
func throwing() throws -> Int { 1 }
func printThrowing() {
guard let int = Optional(
for: try myStruct.succeeding(),
catcher: { error in
// handle the error here
}
) else {
// This is where we usually usually "handle" errors.
// Except we don't actually know what the error is because Swift doesn't
// give it to us here.
}
print(int) // 1
}The obvious question is why not just use try?. Here, try? is definitely easier to read, write
and understand. But there is just one problem:
func printThrowing() {
guard let int = try? throwing() else {
// What's the error?
}
print(int) // 1
}Where's the error? We don't have it because try? never gives it to us. You have never actually handled
the error because you don't even know what it is. If you are confident that you can safely ignore
error then go ahead and use try?. It's a lot more convenient. But if you want to actually handle the
error, then consider using the new Optional initializer.
func throwing() throws -> Int { 1 }
func printThrowing() {
let int = value(default: -1, for: try throwing())
print(int) // 1
}You can parse the error to decide on a replacement value.
func printThrowing() {
let int = value(
for: try throwing(),
replaceErrorWithValue: { e in
// decide how to handle the error here and
// replace with a reasonable value
}
)
print(int)
}Another option is bundling it all in a Result so that you can handle it elsewhere.
func printThrowing() {
let result = result(for: try throwing())
// result is `Result<Int, MyError>`
}If your throwing function returns Void then you can handle the Error in place using doTry().
func printThrowing() {
doTry(
try throwing(),
catching: { e in
print(e)
}
)
}You might be thinking, why not just use a do catch block here like this:
func printThrowing() {
do {
try throwing()
} catch {
print(error)
}
}The truth is, a do catch is better here in most ways. It's shorter, simpler, and easier to read. Here doTry has only one small advantage,
which is that doTry should only be trying one throwing function. (Since doTry accepts an @autoClosure, this makes it very cumbersome to
accidentally try more than one throwing function.)
This means that we know exactly what triggered our catch block.
This library is currently in beta development and is not API stable. The API may change in the future without notice.
Please report any issues you find. In particular, I'm interested in your feedback on the API design. Also, the library supports typed and untyped errors, sync and async functions. Because of this there are many overloaded functions and generic types. Please let me know if the compiler infers an unexpected overload of a function, or if a type inference is missing.
It seems the Swift compiler is not able to infer the error type in some cases. Unfortunately, this severely impacts the ergonomics of the library. For example, the following code will not compile:
struct MyStruct: Sendable {
var int: Int
enum Error: Swift.Error, Equatable, Sendable {
case one, two, three, four
}
mutating func typedThrowing() throws(Self.Error) -> Int {
throw Error.three
}
}
func printThrowing() {
var myStruct = MyStruct(int: 1)
let result = Result(for: try myStruct.typedThrowing())
// Error: Generic parameter 'Failure' could not be inferred
}Even though typedThrowing has a typed error, and the library's Result initializer passes the error type to Result's generic Failure type, the compiler is still unable to infer the error type.
As an unfortunate workaround, you can explicitly specify the error type in the Result initializer.
let result = Result<Int, MyStruct.Error>(for: try myStruct.typedThrowing())This error type inference problem also affects the Optional initializer.
var myStruct = MyStruct(int: 1)
let typedSucceeding = Optional(
for: try myStruct.typedSucceeding(),
catcher: { (error: MyStruct.Error) in
Issue.record("This should not throw")
}
)Here, we must explicitly declare the error type in the catcher closure or else we lose the type information and Swift will treat it as any Error. But we know for certain that the compiler already has the type information because if we explicitly specify the error type then the compiler immediately has the concrete error type without the need for runtime type casting.
This library seeks to solve language-level problems that really should be solved by the Swift language itself. The best solution I have found so far is this pitch:
The library is released under the MIT License.