Donny Wals
Posted on February 24, 2020
This post was originally published on my blog. You can read it in its original form here.
In Swift 2.0, Apple introduced the throws
keyword in Swift. This addition to Swift language added the ability for developers to write code that clearly communicates that an error might occur during the execution of a code path, and it forces the caller of that code to handle, or explicitly ignore the error in-place. In this post I will show you what the throws
keyword is exactly, and how you can deal with errors in your codebase.
Working with code that throws errors
If you've worked with JSONDecoder
in Swift, you have already experienced code that can throw an error. Let's look at a quick example to refresh your mind.
do {
let data = "hello, world".data(using: .utf8)!
let decoder = JSONDecoder()
let string = try decoder.decode(String.self, from: data)
} catch { error in
print("something went wrong!")
print(error)
}
This code is a very basic example of how you can work with code that might throw an error. When you call a function that can throw an error, you must prefix this invocation with the try
keyword. You can't just do this anywhere. Calling out to code that might throw an error must occur in a do {} catch {}
block. The try
prefixed code must be in the do
portion of the block. You can have more than one try
prefixed method call in a single do
block. When any of those method calls throws an error, execution is immediately moved to the catch
part.
The catch
block receives a single argument, which is the Error
that was thrown in your do
block. In Swift, Error
is a protocol that is used to represent errors. It's possible to specialize your catch
block to make it catch only a specific kind of error. If you do this, you still need a general purpose catch
that will catch all other Error
types. Let's look at an example:
do {
let data = "hello, world".data(using: .utf8)!
let decoder = JSONDecoder()
let string = try decoder.decode(String.self, from: data)
} catch is DecodingError {
print("something went wrong while decoding!")
} catch { error
print("something went wrong!")
print(error)
}
You can use pattern matching to specialize catch clauses for an entire category of errors, or a specific error (catch is MyError.someCase
). Doing this can be convenient if you have special handling paths for certain errors, but you do lose access to the original error. The final catch-all is invoked if none of the specialized catch
blocks match the thrown error.
There are cases when you might make the decision that you don't want to handle any thrown errors. In these cases, you can use the try?
prefix instead of try
. When you call a throwing function with try?
, the function's return type becomes an Optional
that's nil
when an error was thrown. Let's look at an example:
enum MyError: Error {
case myErrorCase
}
func doSomething() throws {
throw MyError.myErrorCase
}
print(try? doSomething())
This example is pretty worthless in a real codebase, but it illustrates my point nicely. The doSomething()
function doesn't return anything. This means that it returns Void
, which can also be written as an empty tuple (()
). The printed result of this code is nil
. If you comment out the throw
line from doSomething()
, the printed output is Optiona(())
. In other words, an optional with Void
as its value. If a function has a return value and you call it with try?
, the result is also optional. Let's look at another example:
let data = "hello, world".data(using: .utf8)!
let decoder = JSONDecoder()
let string = try? decoder.decode(String.self, from: data)
print(string)
If you run this code in a Playground, you'll find that the printed value is nil
. The provided data isn't valid JSON, so it fails to decode. But because we call decoder.decode(_:from:)
with try?
, the error is hidden and decode(_:from:)
returns an optional value instead of its normal non-optional value.
If you call a throwing function that returns an optional with try?
, you might expect a nested optional to be returned. After all, the function itself returns an optional and the return type of a throwing function is wrapped by an optional when you call it with try?
. This means that a function that returns String
returns String?
when you call it with try?
. However, if you call a function that returns String?
, it doesn't return String??
. Swift will automatically work around this nested optional, which is both a blessing and a curse. Consider the following code:
let string = try? returnsOptionalString()
if string == nil {
// why is string nil? Is it due to an error? Or did the function execute successfully and we just got back nil without encountering any errors?
}
While the code above might be more convenient to write than a do {} catch {}
block, you lose all error-related information. And in cases where code returns an optional, you don't whether you received nil
because of an error. You should only use try?
if you truly don't care about handling errors, or knowing whether an error occurred at all.
There is one more way to deal with code that can throw errors. You can use try!
in cases where you're absolutely sure that your code shouldn't throw an error, and you want your app to crash if it does. This flavor of try
should be used sparingly, and preferably not at all. Let's look at one last example:
enum MyError: Error {
case myErrorCase
}
func doSomething() throws {
throw MyError.myErrorCase
}
try! doSomething() //
This code would crash at runtime. doSomething()
always throws an error, and by calling it with the try!
prefix we tell Swift that we don't expect doSomething()
to actually throw an error. And when it does, execution of the program should halt, and the app should crash. This is quite radical and, again, should be used sparingly.
Throwing errors in your own code
Sometimes, the code you write needs a way to express that something went wrong and execution of that code path needs to stop immediately with an error. If the error is recoverable, you might have a good candidate for a throwing function on your hands. When I say that an error is recoverable, I mean that the error didn't occur due to a programming error (like accessing an out of bounds index in an array for example) and that the program isn't in an unexpected or invalid state. It might simply mean that something went wrong.
For example, when you try to decode invalid JSON using a JSONDecoder
, that's not considered an error that's severe enough to crash the app. Instead, an error is thrown to let you know that something went wrong. This is an important distinction, and trying to decode invalid JSON should never crash the application. At least not in the JSONDecoder
. You're free to crash your app is a decoding error occurs if you want but I'd strongly advise you not to. Especially if you're loading JSON from a webserver.
When you're writing your own code, you might want to throw an error of your own. You already saw how to do this in the previous section in the doSomething
function:
func doSomething() throws {
throw MyError.myErrorCase
}
Functions that can throw an error must have the throws
keyword appended to their signature, before the return type. Here's what a function signature for a throwing function with a return type looks like:
func returnsOptionalString() throws -> String? {
// do work
}
When you're writing code in a so-called throwing function, you can call methods that throw errors without using a do {} catch {}
block:
func decodeJSON(_ data: Data) throws -> String {
let decoder = JSONDecoder()
let decodedString = try decoder.decode(String.self, from: data)
return decodedString
}
This code is okay because Swift knows that decodeJSON(_:)
might encounter and throw an error. When the JSON decoding fails in this function, the thrown error is passed up to the called of decodeJSON
because it's marked as throwing with the throws
keyword. When this function is called from another function that's throwing, that function will also forward the error up to its caller. The error will be passed up all the way to a caller that's not part of a throwing function.
There is one more error throwing related keyword that I want to show you. It's called rethrows
. The rethrows
keyword is used for functions that don't directly throw an error. Instead, the functions take a closure argument where the closure might throw instead of the function itself. Let's look at an example of a function that takes a throwing closure without rethrows
:
func execute(_ closure: (() throws -> Void)) throws {
try closure()
}
do {
try execute {
print("hello!")
}
try execute {
throw MyError.myErrorCase
}
} catch {
print(error)
}
In the code above I have defined an execute
function. This function takes a single closure argument, and all it does is execute the closure immediately. Nothing fancy. You'll see that both execute(_:)
and the closure it receives are marked with throws
. It's important to understand that marking a function as throwing does not mean that the function is guaranteed to throw an error. All we're saying is that it might. This is especially relevant for the closure argument. The closure that's passed might not even be capable of throwing an error, just like the first call to execute(_:)
in this example. Even though we know that this closure never throws an error, and the compiler also knows it, we must mark the call to execute(_:)
with try
because that function itself might throw an error.
We can clean this code up a little bit by declaring execute(_:)
as rethrowing rather than throwing:
func execute(_ closure: (() throws -> Void)) rethrows {
try closure()
}
execute {
print("hello!")
}
do {
try execute {
throw MyError.myErrorCase
}
} catch {
print(error)
}
Because execute(_:)
is now rethrowing, the Swift compiler can verify whether a code path might throw an error, and it will allow you to call execute(_:)
without try
if it can prove that the closure you passed to execute(_:)
doesn't throw an error. Quite neat right?
You'll find rethrows
in several places in the Swift standard library. For example, map(_:)
is marked with rethrows
because the closure you supply to map(_:)
is allowed to throw errors if needed:
let mapped: [Int] = try [1, 2, 3].map { int in
if int > 3 {
throw MyError.intLargerThanThree
}
return int * 2
}
This probably isn't how you commonly use map(_:)
because typically the closure passed to this function doesn't throw. But now you know that you're allowed to throw errors while mapping, and you also know why you're not forced to mark every call to map(_:)
with try
.
Note that the execution of any throwing method or closure is halted immediately when an error is thrown:
func doSomething() throws {
throw MyError.myErrorCase
print("This is never printed")
}
Throwing an error is a strong signal that something's wrong, and there's no point in fully executing the current code path. The error is sent to the caller of the throwing function, and it must be handled or forwarded from there. If you throw an error in a function that should return something, the function will not actually return anything. Instead, your code switches to the catch
part of the do {} catch {}
block immediately where you can handle the error. The exception here is when the called of your throwing function calls it with try?
or try!
like I explained in the previous section.
In Summary
In this post, I've shown you how you can deal with functions that can throw errors in Swift. You saw how you can call functions that are marked as throwing, how you can tell Swift you're not interested in handling an error and how you can tell Swift that you're absolutely sure a certain call will never actually throw an error at runtime.
After that, I moved on to show you how you can throw errors from your own code, what the rethrows
keyword is and when it's useful.
If you have any feedback for me about this post, or if you have any questions, don't hesitate to reach out to me on Twitter.
Posted on February 24, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 21, 2024
November 18, 2024
November 14, 2024