One of the biggest changes in Swift 2 is that Apple added a feature called error handling. Handling error situations is often the least fun part of programming. It is usually much more exciting to handle a successful case, often referred to as the happy path because that is where the exciting functionality is. However, to make a truly great user experience and therefore a truly great piece of software, we must pay careful attention to what our software does when errors occur. The error-handling features of Swift help us in handling these situations succinctly and discourage us from ignoring errors in the first place.
Before we talk about handling an error, we need to discuss how we can signal that an error has occurred in the first place. The term for this is throwing an error.
The first part of throwing an error is defining an error that we can throw. Any type can be thrown as an error as long as it implements the ErrorType
protocol, as shown:
Let's define a function that will take a string and repeat it until it is at least a certain length. This will be very simple to implement but there will be a problem scenario. If the passed in string is empty, it will never become longer, no matter how many times we repeat it. In this scenario, we should throw an error.
The throws
keyword always comes after the parameters and before a return type.
Now, we can test if the passed in string is empty and throw an error if it is. To do this, we use the throw
keyword with an instance of our error:
An important thing to note here is that when we throw an error, it immediately exits the function. In the preceding case, if the string is empty, it goes to the throw line and then it does not execute the rest of the function. In this case, it is often more appropriate to use a guard
statement instead of a simple if
statement, as shown in the following code:
If we try to call a function, such as normal, Swift is going to give us an error, as shown in the following example:
Now, let's get back to using the try
keyword. There are actually three forms of it: try
, try?
, and try!
. Let's start by discussing the exclamation point form, as it is the simplest form.
The try!
keyword is called the forceful try. The error will completely go away if you use it, by using the following code:
We can also use the try?
keyword, which is referred to as an optional try. Instead of allowing for the possibility of a crash, this will turn the result of the function into an optional:
You can check the result for nil to determine if an error was thrown.
The biggest drawback to this technique is that there is no way to determine the reason an error was thrown. This isn't a problem for our repeatString:untilLongerThan:
function because there is only one error scenario, but we will often have functions or methods that can fail in multiple ways. Especially, if these are called based on user input, we will want to be able to report to the user exactly why an error occurred.
To get an idea of the usefulness of catching an error, let's look at writing a new function that will create a list of random numbers. Our function will allow the user to configure how long the list should be and also what the range of possible random numbers should be.
This function begins by checking the error scenarios. It first checks to make sure that we are not trying to create a list of negative length. It then checks to make sure that the high value of the range is in fact greater than the low one. After that, we repeatedly add a random number to the output array for the requested number of times.
Note that this implementation uses the rand
function, which we used in Chapter 2, Building Blocks – Variables, Collections, and Flow Control. To use it, you will need to import Foundation
and also seed the random number with srand
again.
Also, this use of random is a bit more complicated. Previously, we only needed to make sure that the random number was between zero and the length of our array; now, we need it to be between two arbitrary numbers. First, we determine the amount of different numbers we can generate, which is the difference between the high and low number plus one, because we want to include the high number. Then, we generate the random number within that range and finally, shift it to the actual range we want by adding the low number to the result. To make sure this works, let's think through a simple scenario. Lets say we want to generate a number between 4
and 10
. The range size here will be 10
- 4 + 1 = 7
, so we will be generating random numbers between 0
and 6
. Then, when we add 4
to it, it will move that range to be between 4
and 10
.
So, we now have a function that throws a couple of types of errors. If we want to catch the errors, we have to embed the call inside a do
block and also add the try
keyword:
do { try createRandomListContaininingXNumbers( 5, between: 5, and: 10 ) }
However, if we put this into a playground, within the main
function, we will still get an error that the errors thrown from here are not handled. This will not produce an error if you put it at the root level of the playground because the playground will handle any error thrown by default. To handle them within a function, we need to add catch blocks. A catch
block works the same as a switch
case, just as if the switch
were being performed on the error:
do { try createRandomListContaininingXNumbers( 5, between: 5, and: 10 ) } catch RandomListError.NegativeListLength { print("Cannot create with a negative number of elements") } catch RandomListError.FirstNumberMustBeLower { print("First number must be lower than second number") }
Note that currently there is no way to specify what type of error can be thrown from a specific function; so with this implementation there is no way for the compiler to ensure that we are covering every case of our error enumeration. We could instead perform a switch
within a catch
block, so that the compiler will at least force us to handle every case:
Instead of throwing enumeration cases, we are creating instances of the UserError
type with a text description of the problem. Now, when we call the function, we can just catch the error as a UserError
type and print out the value of its userReadableDescription
property:
Now we can create an enumeration for our specific errors that implements that protocol:
With this, our implementation of the function looks the same as earlier:
Keep in mind that the order of our catch blocks is very important, just like the order of switch cases is important. If we put our UserErrorType
block before the NegativeListLength
block, we would always just report it to the user, because once a catch block is satisfied, the program will skip every remaining block.
The last option for handling an error is to allow it to propagate. This is only possible when the containing function or method is also marked as throwing errors, but it is simple to implement if that is true:
func parentFunction() throws { try createRandomListContaininingXNumbers3( 5, between: 5, and: 10 ) }
However, while this can be a useful technique, I would be careful not to do it too much. The earlier you handle the error situations, the simpler your code can be. Every possible error thrown is like adding a new road to a highway system; it becomes harder to determine where someone took a wrong turn if they are going the wrong way. The earlier we handle errors, the fewer chances we have to create additional code paths in the parent functions.
try!
keyword is
We can also use the try?
keyword, which is referred to as an optional try. Instead of allowing for the possibility of a crash, this will turn the result of the function into an optional:
You can check the result for nil to determine if an error was thrown.
The biggest drawback to this technique is that there is no way to determine the reason an error was thrown. This isn't a problem for our repeatString:untilLongerThan:
function because there is only one error scenario, but we will often have functions or methods that can fail in multiple ways. Especially, if these are called based on user input, we will want to be able to report to the user exactly why an error occurred.
To get an idea of the usefulness of catching an error, let's look at writing a new function that will create a list of random numbers. Our function will allow the user to configure how long the list should be and also what the range of possible random numbers should be.
This function begins by checking the error scenarios. It first checks to make sure that we are not trying to create a list of negative length. It then checks to make sure that the high value of the range is in fact greater than the low one. After that, we repeatedly add a random number to the output array for the requested number of times.
Note that this implementation uses the rand
function, which we used in Chapter 2, Building Blocks – Variables, Collections, and Flow Control. To use it, you will need to import Foundation
and also seed the random number with srand
again.
Also, this use of random is a bit more complicated. Previously, we only needed to make sure that the random number was between zero and the length of our array; now, we need it to be between two arbitrary numbers. First, we determine the amount of different numbers we can generate, which is the difference between the high and low number plus one, because we want to include the high number. Then, we generate the random number within that range and finally, shift it to the actual range we want by adding the low number to the result. To make sure this works, let's think through a simple scenario. Lets say we want to generate a number between 4
and 10
. The range size here will be 10
- 4 + 1 = 7
, so we will be generating random numbers between 0
and 6
. Then, when we add 4
to it, it will move that range to be between 4
and 10
.
So, we now have a function that throws a couple of types of errors. If we want to catch the errors, we have to embed the call inside a do
block and also add the try
keyword:
do { try createRandomListContaininingXNumbers( 5, between: 5, and: 10 ) }
However, if we put this into a playground, within the main
function, we will still get an error that the errors thrown from here are not handled. This will not produce an error if you put it at the root level of the playground because the playground will handle any error thrown by default. To handle them within a function, we need to add catch blocks. A catch
block works the same as a switch
case, just as if the switch
were being performed on the error:
do { try createRandomListContaininingXNumbers( 5, between: 5, and: 10 ) } catch RandomListError.NegativeListLength { print("Cannot create with a negative number of elements") } catch RandomListError.FirstNumberMustBeLower { print("First number must be lower than second number") }
Note that currently there is no way to specify what type of error can be thrown from a specific function; so with this implementation there is no way for the compiler to ensure that we are covering every case of our error enumeration. We could instead perform a switch
within a catch
block, so that the compiler will at least force us to handle every case:
Instead of throwing enumeration cases, we are creating instances of the UserError
type with a text description of the problem. Now, when we call the function, we can just catch the error as a UserError
type and print out the value of its userReadableDescription
property:
Now we can create an enumeration for our specific errors that implements that protocol:
With this, our implementation of the function looks the same as earlier:
Keep in mind that the order of our catch blocks is very important, just like the order of switch cases is important. If we put our UserErrorType
block before the NegativeListLength
block, we would always just report it to the user, because once a catch block is satisfied, the program will skip every remaining block.
The last option for handling an error is to allow it to propagate. This is only possible when the containing function or method is also marked as throwing errors, but it is simple to implement if that is true:
func parentFunction() throws { try createRandomListContaininingXNumbers3( 5, between: 5, and: 10 ) }
However, while this can be a useful technique, I would be careful not to do it too much. The earlier you handle the error situations, the simpler your code can be. Every possible error thrown is like adding a new road to a highway system; it becomes harder to determine where someone took a wrong turn if they are going the wrong way. The earlier we handle errors, the fewer chances we have to create additional code paths in the parent functions.
You can check the result for nil to determine if an error was thrown.
The biggest drawback to this technique is that there is no way to determine the reason an error was thrown. This isn't a problem for our repeatString:untilLongerThan:
function because there is only one error scenario, but we will often have functions or methods that can fail in multiple ways. Especially, if these are called based on user input, we will want to be able to report to the user exactly why an error occurred.
To get an idea of the usefulness of catching an error, let's look at writing a new function that will create a list of random numbers. Our function will allow the user to configure how long the list should be and also what the range of possible random numbers should be.
This function begins by checking the error scenarios. It first checks to make sure that we are not trying to create a list of negative length. It then checks to make sure that the high value of the range is in fact greater than the low one. After that, we repeatedly add a random number to the output array for the requested number of times.
Note that this implementation uses the rand
function, which we used in Chapter 2, Building Blocks – Variables, Collections, and Flow Control. To use it, you will need to import Foundation
and also seed the random number with srand
again.
Also, this use of random is a bit more complicated. Previously, we only needed to make sure that the random number was between zero and the length of our array; now, we need it to be between two arbitrary numbers. First, we determine the amount of different numbers we can generate, which is the difference between the high and low number plus one, because we want to include the high number. Then, we generate the random number within that range and finally, shift it to the actual range we want by adding the low number to the result. To make sure this works, let's think through a simple scenario. Lets say we want to generate a number between 4
and 10
. The range size here will be 10
- 4 + 1 = 7
, so we will be generating random numbers between 0
and 6
. Then, when we add 4
to it, it will move that range to be between 4
and 10
.
So, we now have a function that throws a couple of types of errors. If we want to catch the errors, we have to embed the call inside a do
block and also add the try
keyword:
do { try createRandomListContaininingXNumbers( 5, between: 5, and: 10 ) }
However, if we put this into a playground, within the main
function, we will still get an error that the errors thrown from here are not handled. This will not produce an error if you put it at the root level of the playground because the playground will handle any error thrown by default. To handle them within a function, we need to add catch blocks. A catch
block works the same as a switch
case, just as if the switch
were being performed on the error:
do { try createRandomListContaininingXNumbers( 5, between: 5, and: 10 ) } catch RandomListError.NegativeListLength { print("Cannot create with a negative number of elements") } catch RandomListError.FirstNumberMustBeLower { print("First number must be lower than second number") }
Note that currently there is no way to specify what type of error can be thrown from a specific function; so with this implementation there is no way for the compiler to ensure that we are covering every case of our error enumeration. We could instead perform a switch
within a catch
block, so that the compiler will at least force us to handle every case:
Instead of throwing enumeration cases, we are creating instances of the UserError
type with a text description of the problem. Now, when we call the function, we can just catch the error as a UserError
type and print out the value of its userReadableDescription
property:
Now we can create an enumeration for our specific errors that implements that protocol:
With this, our implementation of the function looks the same as earlier:
Keep in mind that the order of our catch blocks is very important, just like the order of switch cases is important. If we put our UserErrorType
block before the NegativeListLength
block, we would always just report it to the user, because once a catch block is satisfied, the program will skip every remaining block.
The last option for handling an error is to allow it to propagate. This is only possible when the containing function or method is also marked as throwing errors, but it is simple to implement if that is true:
func parentFunction() throws { try createRandomListContaininingXNumbers3( 5, between: 5, and: 10 ) }
However, while this can be a useful technique, I would be careful not to do it too much. The earlier you handle the error situations, the simpler your code can be. Every possible error thrown is like adding a new road to a highway system; it becomes harder to determine where someone took a wrong turn if they are going the wrong way. The earlier we handle errors, the fewer chances we have to create additional code paths in the parent functions.
This function begins by checking the error scenarios. It first checks to make sure that we are not trying to create a list of negative length. It then checks to make sure that the high value of the range is in fact greater than the low one. After that, we repeatedly add a random number to the output array for the requested number of times.
Note that this implementation uses the rand
function, which we used in Chapter 2, Building Blocks – Variables, Collections, and Flow Control. To use it, you will need to import Foundation
and also seed the random number with srand
again.
Also, this use of random is a bit more complicated. Previously, we only needed to make sure that the random number was between zero and the length of our array; now, we need it to be between two arbitrary numbers. First, we determine the amount of different numbers we can generate, which is the difference between the high and low number plus one, because we want to include the high number. Then, we generate the random number within that range and finally, shift it to the actual range we want by adding the low number to the result. To make sure this works, let's think through a simple scenario. Lets say we want to generate a number between 4
and 10
. The range size here will be 10
- 4 + 1 = 7
, so we will be generating random numbers between 0
and 6
. Then, when we add 4
to it, it will move that range to be between 4
and 10
.
So, we now have a function that throws a couple of types of errors. If we want to catch the errors, we have to embed the call inside a do
block and also add the try
keyword:
do { try createRandomListContaininingXNumbers( 5, between: 5, and: 10 ) }
However, if we put this into a playground, within the main
function, we will still get an error that the errors thrown from here are not handled. This will not produce an error if you put it at the root level of the playground because the playground will handle any error thrown by default. To handle them within a function, we need to add catch blocks. A catch
block works the same as a switch
case, just as if the switch
were being performed on the error:
do { try createRandomListContaininingXNumbers( 5, between: 5, and: 10 ) } catch RandomListError.NegativeListLength { print("Cannot create with a negative number of elements") } catch RandomListError.FirstNumberMustBeLower { print("First number must be lower than second number") }
Note that currently there is no way to specify what type of error can be thrown from a specific function; so with this implementation there is no way for the compiler to ensure that we are covering every case of our error enumeration. We could instead perform a switch
within a catch
block, so that the compiler will at least force us to handle every case:
Instead of throwing enumeration cases, we are creating instances of the UserError
type with a text description of the problem. Now, when we call the function, we can just catch the error as a UserError
type and print out the value of its userReadableDescription
property:
Now we can create an enumeration for our specific errors that implements that protocol:
With this, our implementation of the function looks the same as earlier:
Keep in mind that the order of our catch blocks is very important, just like the order of switch cases is important. If we put our UserErrorType
block before the NegativeListLength
block, we would always just report it to the user, because once a catch block is satisfied, the program will skip every remaining block.
The last option for handling an error is to allow it to propagate. This is only possible when the containing function or method is also marked as throwing errors, but it is simple to implement if that is true:
func parentFunction() throws { try createRandomListContaininingXNumbers3( 5, between: 5, and: 10 ) }
However, while this can be a useful technique, I would be careful not to do it too much. The earlier you handle the error situations, the simpler your code can be. Every possible error thrown is like adding a new road to a highway system; it becomes harder to determine where someone took a wrong turn if they are going the wrong way. The earlier we handle errors, the fewer chances we have to create additional code paths in the parent functions.
func parentFunction() throws { try createRandomListContaininingXNumbers3( 5, between: 5, and: 10 ) }
However, while this can be a useful technique, I would be careful not to do it too much. The earlier you handle the error situations, the simpler your code can be. Every possible error thrown is like adding a new road to a highway system; it becomes harder to determine where someone took a wrong turn if they are going the wrong way. The earlier we handle errors, the fewer chances we have to create additional code paths in the parent functions.
So far, we have not had to be too concerned about what happens in a function after we throw an error. There are times when we will need to perform a certain action before exiting a function, regardless of if we threw an error or not.
An important part to remember about throwing errors is that the execution of the current scope exits. This is easy to think about for functions if you think of it as just a call to return. Any code after the throw will not be executed. It is a little less intuitive within do-catch blocks. A do-catch can have multiple calls to functions that may throw errors, but as soon as a function throws an error, the execution will jump to the first catch block that matches the error:
Now if function1
throws an error, the whole program will crash and if function2
throws an error, it will just continue right on with executing function3
.
Now, as I hinted before, there will be circumstances where we need to perform some action before exiting a function or method regardless of if we throw an error or not. You could potentially put that functionality into a function which is called before throwing each error, but Swift provides a better way called a defer block. A defer block simply allows you to give some code to be run right before exiting the function or method. Let's take a look at an example of a personal chef type that must always clean up after attempting to cook some food:
In fact, defer even works when returning from a function or method at any point:
Here, we have defined a small ingredient type and a pantry type. The pantry has a list of ingredients and a method to help us get an ingredient out of it. When we go to get an ingredient, we first have to open the door, so we need to make sure that we close the door at the end, whether or not we find an ingredient. This is another perfect scenario for a defer block.
Ultimately, it is a great idea to use defer any time you perform some action that will require clean-up. You may not have any extra returns or throws when first implementing it, but it will make it much safer to make updates to your code later.
Now if function1
throws an error, the whole program will crash and if function2
throws an error, it will just continue right on with executing function3
.
Now, as I hinted before, there will be circumstances where we need to perform some action before exiting a function or method regardless of if we throw an error or not. You could potentially put that functionality into a function which is called before throwing each error, but Swift provides a better way called a defer block. A defer block simply allows you to give some code to be run right before exiting the function or method. Let's take a look at an example of a personal chef type that must always clean up after attempting to cook some food:
In fact, defer even works when returning from a function or method at any point:
Here, we have defined a small ingredient type and a pantry type. The pantry has a list of ingredients and a method to help us get an ingredient out of it. When we go to get an ingredient, we first have to open the door, so we need to make sure that we close the door at the end, whether or not we find an ingredient. This is another perfect scenario for a defer block.
Ultimately, it is a great idea to use defer any time you perform some action that will require clean-up. You may not have any extra returns or throws when first implementing it, but it will make it much safer to make updates to your code later.
hinted before, there will be circumstances where we need to perform some action before exiting a function or method regardless of if we throw an error or not. You could potentially put that functionality into a function which is called before throwing each error, but Swift provides a better way called a defer block. A defer block simply allows you to give some code to be run right before exiting the function or method. Let's take a look at an example of a personal chef type that must always clean up after attempting to cook some food:
In fact, defer even works when returning from a function or method at any point:
Here, we have defined a small ingredient type and a pantry type. The pantry has a list of ingredients and a method to help us get an ingredient out of it. When we go to get an ingredient, we first have to open the door, so we need to make sure that we close the door at the end, whether or not we find an ingredient. This is another perfect scenario for a defer block.
Ultimately, it is a great idea to use defer any time you perform some action that will require clean-up. You may not have any extra returns or throws when first implementing it, but it will make it much safer to make updates to your code later.