Each and every system has erroneous state, smart system handle those error and fall gracefully, so that the end user don’t just freakout. Swift also provides us with some cool error handling system which can be really worthy when used with try and enum.
On this short blog post we will talk about Swift error handling mechanism. We will concentrate on try to handle those error. Also the reason for choosing enum as the first choice for defining error model.
Background
- Pretalk on Error handling ?
- What is
Error
? - Why
enum
is the first choice for Error definition? - What
throws
rethrows
keywords mean? - How to handling Error?
- Propagation of Error
try
withdo-catch
block- Usage of
try
withdo-catch
block - Optional value through
try?
- Usage of
try?
- Disabling Error propagation through
try!
- Usage of
try!
๐งจ Result<T>
enum
conformingError
protocol, with associated value- Best practices to follow.
We already have some talk over enum. On the enum hub page we can find the details on enum arena.
Pretalk on Error handling
So why do we want Error handling? Well error can happens at any stage even on the most well defined and well architectural app. We have to move on by understanding one true thing, error will occur. How will we handle that error is the important part. Because how would you like an app, which crashes often or regular? No one likes that.
We used to spend some good weeks or even months to build up a moderated app. Just putting some more hours on error handling would make that app a good one. User will get a smooth feelings with the app. Even on the most erroneous situation the app does not crash, this gets the trust from the valuable user. And we all know gaining user’s trust is a crucial part for the long run.
Enough talk, think we all understand the simple thing. Adding proper error handling won’t take long, but give a very high return. Worth investing?
Error
On Swift Error
is an empty protocol, with no requirements to fill. So any type can adapt the Error
protocol.
On Swift only an instance of Error
can be thrown. So we need to adopt the Error
protocol, if we want to use that type as an throwable when error occurs.
Thinks will be more clear on the next section.
enum
as Error
why?
A lot of times we will/had listen that, enum
are perfect for Error
conformation. But why? What is the relation thats makes enum
suitable for Error
.
We can always visits the enum
blog series for more in-depth knowledge on enum
.
Back to our original topic. As we know enum
are pre defined case based architecture, and at a time only one value/case is true for an enum
instance. This goes pretty much same for an Error. At a time only one error can occur among pre define Error set.
Also additional information for a specific error sometimes may be needed. As an example: for age limit verification failure there cloud be overaged
or underaged
Error. enum
‘s associated value cloud serve this extra information. We will have an example on this issue later on this blog post.
throws
and rethrows
Let us talk with the throws
keyword first. When a function has throws
keyword on the definition, that means the function can throw error and the function is called throwing function.
On the other side, when a function has rethrows
keyword on the definition then that function is called rethrowing function. Now to be a rethrowing function that function, should have a functional parameter which is a throwable function. Let us have an example:
enum ReminderError: Error{
case invalidParam
}
var divisor = 0
func hundredReminder() throws -> Int{
if divisor == 0 {
throw ReminderError.invalidParam
}
return 100 % divisor
}
func absReminder(input: Int, reminder: () throws -> Int) rethrows -> Int{
divisor = input < 0 ? abs(input) : input
return try reminder()
}
do {
try absReminder(input: -0, reminder: hundredReminder)
} catch ReminderError.invalidParam {
"Check ur input"
}
On the above example hundredReminder()
is a throwing function as it has the throws
keyword on the definition. And absReminder(input: Int, reminder: () throws -> Int) rethrows -> Int
is a rethrowing function. Have a look at the reminder
params which states that it is a throwing function, thats why the absReminder
needed to be a rethrowing function.
If there is some confusion, don’t tense. I will be clear on this blog post. It is step by step learning. We just clear the throws
and rethrows
concepts. Now lets move to our next topic.
Error Handling
We already have some idea on how to handle errors, and those errors are recoverable error. So the thing is pretty straight forward. We will throw an error if something goes wrong and on the caller we will handle those error to prevent a disaster crash, graceful fall may be . So there are some ways we can handles the error. We will talk about those in the upcoming sections.
Propagation of Error
The concept of throwing an error is called propagating of Error or Error propagation. So each time a throwing function throws
an error it is actually propagating the error. Simple ๐ค.
try
with do-catch
block
On this approach the throwing function is marked with try
and is surrounded by a do-catch
block. Example follows:
enum PrintError: Error{
case InvalidName
case EmptyName
}
func validate(name: String?) throws{
guard let name = name else {
throw PrintError.InvalidName
}
if name.isEmpty {
throw PrintError.EmptyName
}
}
func print(name: String?){
do {
try validate(name: name)
print("Name is: \(name!)")
} catch PrintError.InvalidName {
print("Got an Invalid Name")
} catch PrintError.EmptyName {
print("Got an Empty Name")
}catch {"Other error \(error.localizedDescription)"}
}
print(name: "") //Prints: Got an Empty Name
print(name: nil) //Prints: Got an Invalid Name
print(name: "Mobi Dev Talk") //Prints: Name is: Mobi Dev Talk
Here validate(name: String?)
is the throwing function and on the print(name: String?)
we are handling the error. Inside the do-catch
block we are getting all the defined error of PrintError
including a final catch
case, "Other error \(error.localizedDescription)"
. So now the question is where did the error
comes from? Well error
is an instance of Error
, more appropriately local error constant. This default error
is provided by the Swift error handling system. If none of the above error-patterns matches then this final catch
case with error
will execute. Hmm smart move.
One crucial point: if we do not provide the final catch
case ,with builtin error
var, then there is a potential crash possibility of unhandled error. So we always should provide the final catch
case.
Usage of try
with do-catch
block
- When safety is the major concern.
- When we need to know what specific type of error occurred.
Optional value through try?
Using try?
will provide an optional value, so either there will be a proper value or there will be nil
. The try?
will not provide any error details, rather will provide nil
if any error occurs.
func possiblePrint(name: String?) throws -> String{
guard let name = name else {
throw PrintError.InvalidName
}
if name.isEmpty {
throw PrintError.EmptyName
}
return name
}
var possibleName = try? possiblePrint(name: "")
possibleName?.capitalized // nil
possibleName = try? possiblePrint(name: nil)
possibleName?.capitalized // nil
possibleName = try? possiblePrint(name: "mobi dev talk")
possibleName?.capitalized // Mobi Dev Talk
On the above example possiblePrint(name: String?)
is a throwing function. It will returns a String
if no error is thrown.
We declare possibleName
as a var for reusability. Each time we are assigning possibleName
we are using the try?
. As a result possibleName
is optional String, String?
, even though possiblePrint(name: String?)
returns a String
instance not a String?
. The try?
is converting the proper value of String
to optional string, String?
.
Usage of try?
When we have no concern about the error. We are only interested on the operation or on the value, if the throwing function succeeded.
Disabling Error propagation through try!
This option is the most dangerous one. try!
will disable any error propagation. So if any error occurs immediately the app will crash.
var unwrappedName = try! possiblePrint(name: "mobi dev talk")
unwrappedName.capitalized // Mobi Dev Talk
//unwrappedName = try! possiblePrint(name: "")
//unwrappedName.capitalized // crashs the app
//unwrappedName = try! possiblePrint(name: nil)
//unwrappedName.capitalized // crashs the app
The unwrappedName
has a type of String
as the try!
will forcefully unwrapped the returned value, cloud be either String
or an instance of Error
.
For a valid name the app will not crash but for an empty or a nil value the app will surely crash ๐ฅ.
Usage of try!
๐ฃ When the developer wants to nuke the code base ๐ฅ
Result<T>
There is a common patterns to extract success or fail result. Very useful on asynchronous call. The syntax as follows:
enum Result<Value, Error: Swift.Error>{
case success(Value)
case fail(Error)
}
Here the Value
is a generic type. We can use the above Result
enum as our functional parameter to handle error when we are doing an asynchronous call. The target of this section is to have an introduction with the patterns. For more info we can visit Sundell’s blog which has a beautiful descriptive blog post covering this topic.
enum
conforming Error
protocol, with associated value
One last thing to talk about, before wrapping up this blog post. How associated value can help to provide the additional information for a specific error.
Say we have a registration workflow which has an age limitation, the limit is over 18 and under 30. We want to know why a registration request would fail? Is it because of an invalid username or is it because of an improper age of the user. If the reason is improper age then we would like to know, if the user is overaged or underaged.
So here goes the code:
enum AgeError : String{
case underaged
case overaged
}
enum RegistrationError : Error{
case invalidUserName
case improperAge(AgeError)
}
func registration(userName: String, age: Int) throws {
if userName.isEmpty {
throw RegistrationError.invalidUserName
}
if age < 18 {
throw RegistrationError.improperAge(.underaged)
}else if age > 30{
throw RegistrationError.improperAge(.overaged)
}
}
do {
try registration(userName: "mobidevtalk", age: 0)
} catch RegistrationError.improperAge(let age) {
age //underaged
}
do {
try registration(userName: "mobidevtalk", age: 90)
} catch RegistrationError.improperAge(let age) {
age //overaged
}
As we can see the improperAge
of RegistrationError
has an associated value. On current time it is an enum, AgeError
, having a String raw type.
Here the associated value of improperAge
is providing the additional information, underaged
or overaged
, when there is an age violation on the registration flow.
Best practices, becoming pro
- Choose
try
withdo-catch
block as default error handler. Always provide the finalcatch
case for handling the unspecified errors. - If and only if, you do not need the error then use the Optional value type through
try?
. - Are you expecting some advice to use
try!
? I have only one. Pray to God. God dammit !!! never ever usetry!
on production code.
So that is it for today. The source is used here are shared at GitHub.Our error handling with try
variance is now completed. Happy Error Handling. ๐
3 thoughts on “Error Handling through try variance”