Typed throws are new since Xcode 16 and allow you to define the type of error a method throws. Instead of handling any error, you can handle exact cases and benefit from compiling time checks for newly added instances. They were introduced and designed in Swift Evolution proposal SE-413.
I encourage you to read Try Catch Throw: Error Handling in Swift with Code Examples before diving into typed errors so you’re fully aware of the basics of error handling in Swift. This article will continue on the same path using similar code examples.
Specifying the Error Type
Imagine having the following username validator code that already fails with a consistent type of error:
struct UsernameValidator {
enum ValidationError: Error {
case emptyName
case nameTooShort(nameLength: Int)
}
static func validate(name: String) throws {
guard !name.isEmpty else {
throw ValidationError.emptyName
}
guard name.count > 2 else {
throw ValidationError.nameTooShort(nameLength: name.count)
}
}
}
In all cases of failure, the type will be ValidationError
. This allows us to use typed throws inside the method definition:
/// Using throws(ValidationError) we specify the error type to always be `ValidationError`
static func validate(name: String) throws(ValidationError) {
guard !name.isEmpty else {
throw ValidationError.emptyName
}
guard name.count > 2 else {
throw ValidationError.nameTooShort(nameLength: name.count)
}
}
By defining the expected outcome error, we benefit from compile-time checks and auto-completion. We could rewrite the above method without using the ValidationError
type definition inline:
static func validate(name: String) throws(ValidationError) {
guard !name.isEmpty else {
throw .emptyName
}
guard name.count > 2 else {
throw .nameTooShort(nameLength: name.count)
}
}
Autocompletion is smart enough to detect the type of error and helps you accordingly:
Finally, the compiler will check and throw an error if you’re trying to throw a different kind of error:
Since we’re trying to throw OtherError
instead of ValidationError
, we’re running into the following error:
Thrown expression type ‘UsernameValidator.OtherError’ cannot be converted to error type ‘UsernameValidator.ValidationError’
Compile-time checks are highly valuable and will also occur when handling errors inside a do-catch clause.
Handling typed throws inside a do-catch clause
The introduction of typed throws also added new functionality for do-catch clauses. You can explicitly define the expected error as follows:
do throws(UsernameValidator.ValidationError) {
try UsernameValidator.validate(name: name)
} catch {
switch error {
case .emptyName:
print("You've submitted an empty name!")
case .nameTooShort(let nameLength):
print("The submitted name is too short!")
}
}
This allows you to switch on the thrown error and handle all cases. If any new cases get added in the future, you’ll be notified by the compiler with a “Switch must be exhaustive” compile-time failure.
Specifying the error type inside a do-catch clause is only valuable in case you want to prevent the same do-closure from throwing other errors:
Swift is smart enough to inherit typed throws from methods inside the do-clause. In other words, we could write the above method as follows and still benefit from autocompletion:
do {
try UsernameValidator.validate(name: name)
} catch {
switch error {
case .emptyName:
print("You've submitted an empty name!")
case .nameTooShort(let nameLength):
print("The submitted name is too short!")
}
}
Conclusion
Typed throws are a valuable addition to Swift, allowing us to write more predictable code. SDKs, in particular, can benefit from this feature by better predicting the error to expect. More compile-time checks help us avoid forgetting about handling any new error cases in the future.
If you like to improve your Swift knowledge, even more, 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!