In this tutorial we cover the following topics
In Swift, errors are represented by values of types that conform to the
Error
protocol. This empty protocol indicates that a type can be used for error handling.
A good choice to represent a group of related error types are enumerations.
A throw
keyword is used to bring to life an error.
1 2 3 4 5 6 7 8 9 |
import Foundation enum ErrorColection: Error { case errorType0 case errorType1 case errorType2 } throw ErrorColection.errorType1 |
When an error is thrown, some surrounding piece of code must be responsible for handling it. There are four ways to handle errors in Swift.
- Error can be propagated from a function to the code that calls that function.
- Error can be handled with
do-catch
statement. - Error can be handled as an optional value.
- Error propagation can be disabled and its call wraped in a runtime assertion that no error will be thrown.
We use the
throws
keyword to indicate that a function, method, or initializer can throw an error. A function marked this way is called a throwing function. Only throwing functions can propagate errors. Any errors thrown inside a nonthrowing function must be handled inside that function.
1 2 3 4 5 6 7 8 |
class classThrowingErrors { func functionThrowingErrors(forNumber number: Int) throws { if (number == 0) { throw ErrorColection.errorType0 } print("number: \(number)") } } |
do-catch
General form of
do-catch
statement is very rich
1 2 3 4 5 6 7 8 |
do { try expression statements } catch pattern_1 { statements } catch pattern_2 where condition { statements } |
and can be used as the following example shows
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
class classThrowingErrors { func functionThrowingErrors(forNumber number: Int) throws -> Int{ if (number == 0) { throw ErrorColection.errorType0 } print("number: \(number)") return number } func functionNoThrowingErrors(forNumber number: Int) { do { let x = try functionThrowingErrors(forNumber: 0) print("No errors, result is \(x)") } catch ErrorColection.errorType0 { print("Error type 0") } catch ErrorColection.errorType1 { print("Error type 1") } catch ErrorColection.errorType2 where number > 4 { print("Error type 2") } catch let error { // Who knows, maybe there are more errors possible? // We have to catch all of them. print(error.localizedDescription) } } } var c = classThrowingErrors() c.functionNoThrowingErrors(forNumber: 0) |
Error type 0
We use
try?
to handle an error by converting it to an optional value. If an error is thrown while evaluating the try?
expression, the value of the expression is nil
.
1 |
var e = try? c.functionThrowingErrors(forNumber: 0) |
When we are sure that throwing function or method will not throw an error at runtime we can write
try!
to disable error propagation and wrap the call in a runtime assertion that no error will be thrown. Note that if an error actually occurs and is thrown, we will get a runtime error.
1 |
var t = try! c.functionThrowingErrors(forNumber: 1) |
In Java very common statements sequence is
try-catch-finally
becauseThe finally block always executes when the try block exits. This ensures that the finally block is executed even if an unexpected exception occurs. But finally is useful for more than just exception handling — it allows the programmer to avoid having cleanup code accidentally bypassed by a return, continue, or break. Putting cleanup code in a finally block is always a good practice, even when no exceptions are anticipated. [finally doc]
In Swift there is very similar statemen: defer
. It is used to execute a set of statements just before code execution leaves the current block of code. As in Java with this statement we can do any necessary cleanup that should be performed regardless of how execution leaves the current block of code - whether it leaves because an error was thrown or because of a statement such as return or break or even quite natural without any errors or jumps. The most basic example given in all tutorials is when we want to ensure that file descriptors are closed or manually allocated memory is freed.
A defer statement defers execution until the current scope is exited. The deferred statements may not contain any code that would transfer control out of the statements, such as a break or a return statement, or by throwing an error. Deferred actions are executed in the reverse of the order that they’re written in our source code. That is, the code in the first defer statement executes last, the code in the second defer statement executes second to last, and so on. The last defer statement in source code order executes first.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
func doSomethingWithResources() throws { print("1") defer { print("3") } defer { print("5") } print("2") throw ErrorColection.errorType0 print("6")// Code after 'throw' will never be executed defer { print("4") } } defer { print("9") } do { try doSomethingWithResources() } catch { print("7") throw ErrorColection.errorType0 } defer { print("8") } |
1
2
5
3
7
9
fatal error: Error raised at top level:
[...]
If you want to see more examples, you can read Magical Error Handling in Swift