Errors

There are times when, for some reason, you will not be able to finish some process. No matter the reason, could be a violation of preconditions or some error with the state, in these cases, it would be ideal to not crash the app, but provide some helpful errors to explain what went (or is going) wrong.

Designing Errors

There are no built in errors in Swift. You can use assert(_: Bool) or fatalError(_:), but this doesn't really count. We can make any type an Error by adopting the Error protocol–no extra work needed! Generally speaking, we want to list all of the potential errors, which means you should usually be working with enums. If you need more information and don't want to use enums with associated values, you can store an enum as a member inside a larger object/value. But generally speaking, enums are the best practice.

We can imagine when working with networking, that the following issues may arise:

enum NetworkError {
    case invalidURL(String)
    case serverError
    case invalidData
    case poorConnection
}

Note: you'd normally use the URL type, but this is only for demonstration purposes, so do not use this XD

Raising Errors

Now that we have built an error, raising them is super easy! Just create an instance, and write throw <error>. With the example above, we could do:

throw NetworkError.serverError

Marking as Error-Prone

As with everything in Swift, we need to be explicit about what is going on. Thus, the potential for errors becomes part of types. If a function can raise an error, we say that it throws. We write the throws right after the parameters of a function, but before the -> for return type if there is one.

func network() throws -> Data {
    ...
}

Handling Errors

If a process is marked as throwing, you will need to acknowledge the error in how you handle it. We have three methods to deal with errors, but they can be slightly confusing because they all use the try keyword.

Default Try

This is probably pretty similar to how you have handled errors in your other languages. We use a do-catch block. Every time there is a potential error in a do block we must use some try.

do {
    let someData = try network()
    ...
    let someMoreData = try network()
    ...
}
...

The catch, where you actually handle the error, is very flexible. We can categorize instructions by the type of error. You can think of this almost like a switch block, except you don't need to handle all of the potential cases (because there could be so many types of errors).

Catching Enum Cases

We can catch a specific enum case just by using catch followed by the case:

... } catch NetworkError.serverError {}

You can also use a let-statement to bind an associated value.

... } catch NetworkError.invalidURL(let url) {}

Catching Types

If you do not care about the specific information in an error, just about its type then you can use the is keyword:

... } catch is NetworkError { ... }

Additional Classification of Types

If you want a certain block to execute given a type, but you actually need further differentiation (and could not get that with the above methods) you can do a conditional assignment. I think of this as a where clause.

... } catch let e as NetworkError {
    switch e { ... }
}

I'm just using switch as an example–you can do whatever you want.

You may gravitate towards this option if you have chosen to use a class or struct for your error.

Default

In the case where you don't catch an error, it will automatically be raised (i.e. you didn't handle it, so it will run free and stop your program). As with switches, there is a default case. Just write catch. This will assign the error to the identifier error:

... catch {
    // error: Error = ...
}

try?

Catching can be a lot. What if you only want to do something if no error is thrown and just ignore any other case? As the name implies, try? convert our results into optionals! If there is no error, it returns an optional and otherwise a nil value. In this case, we don't need a do-catch block.

let myData: Data? = try? network()

try!

As you may have guessed, this is the force-unwrapped version of errors. We are guaranteeing that whatever we are doing, even though it is marked as throws, will never raise an error. We are so convinced of this fact, that if we are wrong, we are okay with the program crashing. You do not need a do-catch block.

let myData: Data = try! network()

Last updated