Swift is a powerful, expressive programming language with a flexible, elegant syntax. One of the features I appreciate most is its native support for error handling. In this post, you learn about throwing functions and how try
, try?
, and try!
are different.
Ignoring Errors in Objective-C
It is easy to ignore errors in Objective-C. In fact, you need to do a bit of extra work to enable error handling.
NSError *error = nil;
// Initialize Data With Contents of URL
NSData *data = [[NSData alloc] initWithContentsOfURL:URL options:0 error:&error];
if (error) {
// Error Handling
...
} else {
...
}
It is easy to ignore errors in Objective-C as illustrated in the following example.
// Initialize Data With Contents of URL
NSData *data = [[NSData alloc] initWithContentsOfURL:URL options:0 error:nil];
if (data) {
...
}
Catching Errors in Swift with Try
Swift is much stricter when it comes to error handling and that is a good thing. The syntax, for example, is much more expressive. The try
keyword indicates explicitly that a function or method may throw an error. To catch and handle an error, the throwing function or method call needs to be wrapped in a do-catch
statement.
do {
// Initialize Data with Contents of URL
let data = try Data(contentsOf: url)
...
} catch {
// Error Handling
...
}
Errors thrown in the do
clause are caught and handled in the catch
clause. That is error handling in Swift in a nutshell.
Try, Try?, and Try!
Some developers are confused by the different flavors of the try
keyword. Swift defines three variations of the try
keyword.
try
try?
try!
You already know how to use the try
keyword, but how does it differ from try?
and try!
?
Failing Silently with try?
The question mark of the try?
keyword is a subtle hint. By using the try?
keyword, the throwing function or method fails gracefully. Let's revisit the previous example and replace try
with try?
.
// Initialize Data with Contents of URL
let data = try? Data(contentsOf: url)
Note that we no longer need a do-catch
statement. Why is that? The type of data
provides the answer. Press Option and click the data
constant in Xcode. you should see something like this.
The initializer of the Data
struct no longer returns a Data
object. The data
constant is of type Data?
, an optional. The idea is simple. If the initializer throws an error, the error isn't propagated to the call site so it isn't possible to catch it. Instead the initializer returns nil
, failing silently.
I wrote earlier that it is easy to ignore errors in Objective-C. You can ignore errors in Swift, but you still need to use the try
keyword to indicate that the function or method you invoke can silently throw an error.
The downside of the try?
keyword is that the result of the Data
initializer is of an optional type so you need to unwrap it to access the Data
object. The try?
pattern works well in combination with optional binding as shown in the following example.
// Initialize Data with Contents of URL
if let data = try? Data(contentsOf: url) {
print("Success")
} else {
print("Failure")
}
Note that we invoke the init(contentsOf:options:)
initializer without wrapping it in a do-catch
statement. An optional is returned and, if successful, its value is bound to the data
constant.
// Initialize Data with Contents of URL
if let data = try? Data(contentsOf: url) {
print("Success")
} else {
print("Failure")
}
The try?
pattern also works well with a guard
statement.
func loadContentsOfFile(at url: URL) -> String? {
guard let data = try? Data(contentsOf: url) else {
return nil
}
return String(data: data, encoding: .utf8)
}
Failing Gloriously with try!
More dangerous is the use of the try!
keyword. In Swift, an exclamation mark always serves as a warning sign. There is a reason the Swift team chose an exclamation mark instead of a 🐶 emoji. By appending an exclamation mark to the try
keyword, error propagation is disabled.
If an error is thrown, your application crashes as the result of a runtime error. As a general rule, use try
or try?
, and avoid or minimize the use of try!
.
When can you or should you use try!
? It is perfectly fine to ignore try!
altogether. If you are absolutely certain that a throwing operation will not throw an error at runtime, then it is safe to use try!
. I personally never use the try!
keyword in production code. I do sometimes use it in unit tests, for example, to load stub data.
The use of try!
is similar to the use of implicitly unwrapped optionals. If you are absolutely certain that an optional contains a value, you can safely use an exclamation mark to force unwrap the value of the optional. But consider the exclamation mark as a warning that you are performing an operation that may cause your application to crash.
Question Marks and Exclamation Marks
As I wrote earlier, the Swift team chose the question mark and the exclamation mark for a reason. A question mark indicates that you are not certain or confident about something. An optional, for example, may or may not contain a value.
The exclamation mark is a warning sign. You are performing a potentially dangerous operation. You are forcing something by taking a shortcut.
Even though it usually requires a few more lines of code, it is recommended to use try
and try?
instead of try!
. The same applies to unwrapping optionals. That said, there certainly are use cases that justify the use of try!
and force unwrapped optionals.