Error Handling in Swift 2
Exploring Swift's do-try-catch syntax for error handling
First of all, thanks for having me here. It's my first time in the USA, so, it's a very exciting week for me.
Let me introduce myself to you. I work as a mobile developer in Germany at Arvato Systems S4M. It is an IT service supplier and I work in the mobile department and I'm working most of the time on native iOS projects. I'm blogging at thomashanning.com about iOS development, if you want to check it out. And you can find me on Twitter as well @hanning_thomas. Okay. Let's start with error handling and first of all, I want to ask you a question; Who of you love error handling?
Error handling is not very popular, but it is very important
Well, that's more than I expected, but not so many. That is actually true- error handling is not very popular, but it is very important. And there is a reason for it not being so popular, because developers tend to only think about the best case and they don't want to think about the fact that something could go wrong. But in reality, there is always something going wrong, so, error handling is important. The good news is, that since Swift 2.0, there is a complete new error handling system in Swift and it replaces the old NSError way, that was used in Objective-C and before 2.0 also in Swift and NSError is a good way but its also very special. It's a lot about implicit conventions. So, it was not so easy to use.
...developers tend to only think about the best case and they don't want to think about the fact that something could go wrong.
Now, Swift has a do-try-catch syntax for error handling. So, it's a lot more like modern languages like Java and C#. There are, of course, differences, but it's much more modern now. Okay, let's start. So, this is a very basic topic talk. I will explain how you can use the error handling system and we start by defining an error, because if you want to throw an error, first you have to define it. Actually, every type that implements the ErrorType protocol, can be an error. And what is this protocol? Well, surprisingly, it is actually empty. So, if you are looking at the API documentation, there's nothing to see.
But, it automatically, adds for example, the property ‘_code’, behind the scenes. And, if you are familiar with NSError, then you know that _code is also a property of NSError, and NSError, itself conforms to the ErrorType protocol. That's important for the interoperability between Swift and Objective-C. But the best choice for an error are actually enums and this is a very basic example. We have an enum called ‘AnError’ that implements the ErrorType protocol without doing anything more, other than mention it and it has two error cases, Type 1 and Type 2. And, that's it, we have an error type.
...the best choice for an error are actually enums...
So, now we have an error and we want to throw it. So, if one function wants to throw an error, it has to declare it. And, it does it by using the new keyword "throws". So, we have here a function 'doSomethingRisky', and a new keyword. And this is perfectly fine, there will be no compiler error. So, just because a function announces that it is able to throw an error, it does not have to really throw an error. But we want to do it and we are doing this by using the keyword "throw," which is also new in Swift 2.0. So, our example is a little bit expanded. Now, we have a parameter of type Int, and if "I" is smaller than 5, then we are throwing error Type 1 and if it's bigger than five and smaller than 10, then we are throwing error Type 2.
And you see immediately that the code is very easy. It's just one line of code and you don't have to do any other things like you would have done it with NSError. So, very easy, catching an error... If you're calling a function that can throw an error, you have to catch it. And you are doing this by using the do-try-catch syntax. So, first of all, before the function crawl, you are using the keyword "try" and you embed in the ‘do’ block and if a function occurs, then the code execution after the function crawl is stopped, and you are jumping right into their catch block. So, in this example, one is smaller than five, so, we are jumping immediately in the catch block. Again, very, very easy syntax.
...you can see that there is a pure catch block as well, and this is because a do-try-catch must be exhaustive.
But, it is also possible to have more than one error case. So, in this example we have expanded error Type 1 and error Type 2. So, depending on whatever is thrown, the corresponding catch block is executed. But you can see that there is a pure catch block as well, and this is because a do-try-catch must be exhaustive. And now, you might say, "Well, we know that just our defined error can be thrown, so we don't have to have the pure catch block". But the function only announces that it can throw an error but not what type of error. So, the compiler doesn't know it. So, we have to implement that pure catch case, and on the first sight, this might seem like a big disadvantage because in other languages like Java, you can say that a function throws a certain type of error. But, if you think about it, then you see that it has one big advantage and that is, if you are changing the error throwing code, for example, if you are adding type three, then existing code does not break. So, this is really a very, very big advantage, you can expand your code but you don't have to do it. And, that's the reason why function does not announce what kind of error is thrown.
Okay, cleaning up. Sometimes, or very often, you want to clean up before a function returns. For example, you might want to close a file or a database. But since the function is able to return at different positions, that is not an easy task. So, in fact, if we are doing it without any new features of Swift 2.0, we would do the following: Our clean up is just the call of one function that is called cleanUp. And we are placing the function crawl at three different positions; One at the end of the function, which would be the normal exit for the function, and two at positions where the error is thrown. So that is very bad because if you're repeating yourself in programming, then you're doing something wrong. Fortunately, there is a new keyword called 'defer' in Swift 2.0, and with defer you can declare a block of code that is always executed just before the method returns and it doesn't matter why the method returns.
...if you're repeating yourself in programming, then you're doing something wrong.
So, in our example, we can just place a cleanUp call at the beginning of the function in the defer block and everything works fine. There are three important things about defer. First, you can put it at every position in the function; at the beginning, at the end, in the middle. And you can always access all local variables, even if they are declared after the defer block. The third thing is, you can define more than one defer block and they are then executed in reverse definition order. I personally think that the best position for a defer block is at the beginning of the function because then anyone who is reading your code immediately knows what is going on and you're safe. And when you're looking at your code after a few weeks, sometimes you don't know what is going on, but here you would know exactly what is happening.
You can use try! and when you use it, you don't have to catch errors.
Okay, ‘try!’ You can use try! and when you use it, you don't have to catch errors. So, in our example, we would just call the function 'doSomethingRisky' and in front of it, we have try! and no ‘do’ and no ‘catch’ is needed. Sounds good, but the application will crash if an error is thrown. So, you should only use it if you're very sure that there is no error and there are use cases for that. For example, if you're accessing a file in the app bundle, then you can be very sure if it's working at development time that it will always work. And if it's not working, then there is something completely wrong and an app crash is, in this case, a good solution. But generally speaking, only use it if you are very sure what you're doing and if you're not sure, don't use try!.
Interoperability. Well, we know that all of Apple’s frameworks are written in Objective-C, so there is still a lot of NSError code and it would be very bad if the interoperability between NSError and do-try-catch wasn’t good. Fortunately, the interoperability is good. So, this is an Objective-C example, I don't want to go much into details here, but the point here is that an error can be thrown by the NSError parameter in the function hit and by returning a “no” if a function occurs.
Fortunately, the interoperability is good.
Now, if you want to use an Objective-C class that you've written on your own, you have to declare it in the bridging header file of the project, and then all API is translated to Swift syntax. If you do this for this function, then they will write as the following. The return type is changed from ‘bool’ to ‘void’ and also the NSError parameter is gone. And it has the keyword "throws". That means that we can call the method as we know it with do-try-catch and we can also access the NSError parameter in the catch block.
Migration to Swift 2.0. We know that step one, Swift 1.2 to 2.0, was not as big as the step before (Beta to GM). But there are a lot of things that need to be done to get the code working. You have to do this if you want to use Xcode 7, Swift 2.0 and the iOS 9 SDK. So, you could still use Xcode 6.4 but that is not a very good advice. One of the main things that's changing in Swift 2.0 is the error handling code. So, how do you make the migration? Well, there is a migration tool that Xcode offers and it does most of the work for you.
...there is a migration tool that Xcode offers and it does most of the work for you.
So, if you're running the migration tool then Xcode migrates the code and you see a diff and you can see if everything is fine. You should check it very carefully because although the migration tool is good, it can not know what your intention was of the code. So, you should check it. And even if your check is okay, then there will still be a lot of compiler errors and you have to do some tweaks on your own. A lot of people are complaining about this fact that you have to migrate your existing code, but in my opinion, it is good that the language is improving, even if it means that you have to change existing code. Also, you have to remember that if you are supporting a new SDK, for example, the iOS 9 SDK, you have to make code changes anyway. So it's a little bit more work, but you have to do work anyway. Okay. Yes, I've said that you should check it carefully.
Summary already. First of all, even if you don't love error handling, it is very important. Swift has a completely new error handling model that is easier to use than the old NSError way and the operability between Swift and Objective-C, in this case, is very good. So, thanks. Any questions?
...even if you don't love error handling, it is very important.
Q1: So, the do-try-catches need to be exhaustive? Could you explain a bit more what determines the cases you have to cover, since, I imagine, you can have multiple types that inherit from NSError? And, could you explain how to make sure you're exhaustive?
Thomas: Oh, it's very easy. You just have to have a pure catch block and then, it is exhaustive, and all errors that are not handled by the other catch blocks are handled in the pure catch block.