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.

What Is The Difference Between Try, Try?, And Try!

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.