You use do
, try
, catch
and throw
to handle errors and exceptions in Swift. Error handling gives you greater control over unexpected scenarios that might occur in your code, like a user that inputs a wrong account password.
In this tutorial, we’ll discuss:
do try catch
Error
types (and why)try?
try!
to disable errors (and its risks)Ready? Let’s go.
Some errors and bugs in your app are the result of a mistake the programmer makes. For example, when your app crashes with Index out of bounds, you probably made typo somewhere. When you force unwrap an optional that’s nil
, your app inevitably crashes.
Ideally, your app has 0 of these bugs. Your job as a coder is to find and fix them before your app is published in the App Store. In this tutorial, we’re going to talk about a different kind of error.
In practical iOS development, not all errors are bad. Some errors are part of an app’s lifecycle, such as an “Incorrect password” exception when you accidentally try to log in with the wrong account credentials.
Errors like these are recoverable. You’ll want to catch the error and respond appropriately, like showing the user an alert dialog that informs them about the error.
A few examples:
You can recover from these errors by displaying an alert message, or doing something else. Your credit card may get blocked after 3 failed attempts, for example. Your car can point you to the nearest fuel pump station when you’re out of gas. And you can try to log in with a different username and password.
It’s worth noting here that these errors do not result in a crash that quits the app. It would suck if an ATM machine would need to be restarted if you entered the wrong PIN code! This distinction is important; between recoverable errors and actual bugs that need to get fixed.
As you’ll soon learn, you handle errors in Swift with the do
, try
, catch
and throw
keywords, as well as Swift’s native Error
type. Swift has first-class support for handling errors, which means that its implemented at a language-level and that it has the same kind of control as if
, guard and return
, for example.
Let’s move on!
Before we discuss handling errors, let’s first take a look at throwing them. That happens with the throw
keyword in Swift. Every error or exception ‘starts’ when its thrown.
Check this out:
if fuel < 999 {
throw RocketError.insufficientFuel
}
In the above code, the throw
keyword is used to throw an error of type RocketError.insufficientFuel
when the fuel
variable is less than 999
.
In other words, we’re throwing that error when, while running the code, it so happens that there’s not enough fuel. Throwing an error lets you indicate that something unexpected happened.
When you wrote this code in your app, you anticipated that this exception could happen, which is why you put that conditional and throw
code in there.
Imagine we’re trying to launch a rocket. Like this:
func igniteRockets(fuel: Int, astronauts: Int) throws
{
if fuel < 999 {
throw RocketError.insufficientFuel
}
else if astronauts < 3 {
throw RocketError.insufficientAstronauts(needed: 3)
}
// Ignite rockets
print("3... 2... 1... IGNITION!!! LIFTOFF!!!")
}
The function igniteRockets(fuel:astronauts:)
will only ignite the rockets if fuel
is greater or equal to 999
and if there are at least 3 astronauts on board.
In the above code we’re using an error type called RocketError
. Here’s what it looks like:
enum RocketError: Error {
case insufficientFuel
case insufficientAstronauts(needed: Int)
case unknownError
}
This enumeration extends the Error
protocol. It defines three types of errors: .insufficientFuel
, insufficientAstronauts(needed)
and .unknownError
. You can only use throw
with a value that’s represented by the Error
type. Defining your own error types is super useful, because you can be clear about what these errors mean in your code.
Take for instance the insufficientAstronauts(needed:)
error. When this error is thrown (see above code) you can provide an argument that indicates how many astronauts are needed to successfully ignite the rocket. Neat!
The throw
keyword has the same effect as the return
keyword. When throw
is invoked, execution of the function stops at that point and the thrown error is passed to the caller of the function. At that point it needs to get handled with a do try catch
block, which we’ll discuss in the next section.
The big question is, of course, could you implement the same functionality without making use of throw
? For example, like this:
func igniteRockets(fuel: Int, astronauts: Int)
{
if fuel >= 999 && astronauts >= 3 {
print("3... 2... 1... IGNITION!!! LIFTOFF!!!")
} else {
// ... do what!?
}
}
Notice the difference with the previous function? This is an architectural question worth thinking about.
Error handling with throw
, try
and catch
is a tool that can help you make your code easier to read, maintain and extend. You have a few alternatives at your disposal, so – as with anything in coding – the devil is in the details. What tool serves you best?
One last thing – the igniteRockets(···)
function is marked with the throws
keyword. It indicates to whoever calls this function that errors must be handled. Swift forces us to handle errors (or rethrow them), which means you can’t accidentally forget it! It’s a feature that makes Swift safer to use, with fewer errors, because you catch them sooner.
Speaking of handling errors, let’s discuss that next!
In Swift, you handle errors with a do-try-catch
block, to take appropriate action when an error is thrown.
Here’s an example:
do {
try igniteRockets(fuel: 5000, astronauts: 1)
} catch {
print(error)
}
Handling errors with do try catch
has 3 important aspects:
try
keywordtry
in a do { ··· }
blockcatch { ··· }
blocks to handle all or individual errorsIn many other programming languages, like Java or C++, this approach to handle errors is called try/catch (without do
). The expression that can throw errors isn’t explicitly marked with try
. Swift makes this explicit.
Looking at the above code, you can imagine what happens next. The igniteRockets()
function throws an error, and the catch
block is subsequently invoked. This prints out the error with print(error)
.
It’s good to know that, even though no constant with the name error
has been declared in the catch
block, this value is available implicitly. Within a catch
block, you can use the error
constant to get the error that’s thrown.
You can also declare the error value explicitly, like this:
do {
···
} catch(let exception) {
print(exception.localizedDescription)
}
With do-try-catch
, you can also respond to error cases individually. Remember the RocketError
enum we defined earlier? Now check this out:
do {
try igniteRockets(fuel: 5000, astronauts: 1)
} catch RocketError.insufficientFuel {
print("The rocket needs more fuel to take off!")
} catch RocketError.insufficientAstronauts(let needed) {
print("You need at least \(needed) astronauts...")
}
The above catch
blocks are invoked based on enumeration cases of RocketError
. Its syntax is similar to that of a switch block. You can directly access associated values of enum cases, such as the needed
constant in the above example.
You can also use expressions and pattern matching with where to get more control over the catch
block that’s triggered for an error.
Why don’t you give it a try yourself?
[sandbox]
enum RocketError: Error {
case insufficientFuel
case insufficientAstronauts(needed: Int)
case unknownError
}
func igniteRockets(fuel: Int, astronauts: Int) throws
{
if fuel < 999 {
throw RocketError.insufficientFuel
}
else if astronauts < 3 {
throw RocketError.insufficientAstronauts(needed: 3)
}
// Ignite rockets
print(“3… 2… 1… IGNITION!!! LIFTOFF!!!”)
}
do {
try igniteRockets(fuel: 5000, astronauts: 1)
} catch {
print(error)
}
[/sandbox]
Quick Tip: You can provide a textual representation of an error by creating an extension for your custom error type, conforming to the LocalizedError protocol. Use the extension to implement the errorDescription
, failureReason
, helpAnchor
and recoverySuggestion
properties. Iterate over the error enum, and return string values that describe the error.
The purpose of error handling is to explicitly determine what happens when an error occurs. This allows you to recover from errors, instead of just letting the app crash.
In some cases, you don’t care much about the error itself. You just want to receive value from a function, for example. And if an error occurs, you’re OK with getting nil
returned.
You can do this with the try?
keyword. It’ll convert any error that occurs into an optional value. The syntax combines try
with a question mark ?
, in a similar fashion as working with optionals. When you use try?
, you don’t have to use the complete do-try-catch
block.
Here’s an example:
let result = try? calculateValue(for: 42)
Imagine the calculateValue(for:)
function can throw errors, for example if its parameter is invalid. Instead of handling this error with do-try-catch
, we’re converting the returned value to an optional.
One of two things will now happen:
result
result
is nil
Handling errors this way means you can benefit from syntax specific to optionals, such as the nil-coalescing operator ??
and optional binding. Like this:
if let result = try? calculateValue(for: 99) {
// Do something with non-optional value "result"
}
Using nil-coalescing operator ??
to provide a default value:
let result = try? calculateValue(for: 123) ?? 9000
Be careful with using try?
too often. It’s compelling to use try?
to ignore or silence errors, so don’t get into the habit of using try?
to avoid having to deal with unexpected errors.
Error handling with do-try-catch
is a feature for a reason, because it generally makes your code safer and less prone to errors. Avoiding to recover from errors only leads to (quiet) bugs later on.
What if you’re coding a function that includes a call to another function that can throw errors, but you don’t want to deal with the error right there? Mark your own function with rethrows
. Like its name implies, this will “re-throw” the error to the caller of your function.
You can also disable error handling entirely, with try!
. This effectively stops the propagation of the error – and crashes your app.
Just as the try?
keyword, the try!
syntax combines try
with an exclamation mark !
, in a similar fashion to force unwrapping optionals. When you use try!
, you don’t have to use the complete do-try-catch
block.
Unlike try?
, which returns an optional value, the try!
syntax will crash your code if an error occurs. There are 2 distinct scenarios in which this is useful:
try!
when your code could impossibly result in an error, i.e. when it’s 100% certain – from the context of your code – that an error cannot occurtry!
when you can’t recover from an error, and it’s impossible to continue execution beyond that pointA few examples:
try!
to load the image – because that missing file error is never thrown.try!
to load that database into memory, because when the database is corrupted, the app won’t be usable anyway.Handling errors with do-try-catch
helps you to write more robust and fault-tolerant code. You can use it to recover from errors, for example by displaying an error message to the user.
With syntax like try?
and try!
you can write more concise code. And custom error types give you the opportunity to communicate what kind of error has occurred, and respond accordingly. Neat!
A few tutorials pair exceptionally well with this one. They are:
Want to learn more? Check out these resources: