Difference Between try, try? and try! and When to Use Them in Swift

Chase
4 min readMay 20, 2024

--

If someone asked you right now to explain the differences between the various version of try in Swift and when to use each, could you do it? In this tutorial, we will go over what each option does, and when is the best time to use each one.

the text of try, try?, try! on a background absent of color

Before we get started, please take a couple of seconds to follow me and 👏 clap for the article so that we can help more people learn about this useful content.

Quick background on try

The try keyword in Swift is used to help keep our code as safe as it can be. It is a way for us as the developer to handle errors that may be thrown in our code. For example, let’s say that we had an error that we wanted to throw (called MyError) and we had a function that could throw that error (called thisThrowingFunction). If we wanted to call this function that throws errors, we would want to be able to handle the error that is thrown from this function, this is referred to as error handling.

import Foundation

struct MyError: Error {}

func thisThrowingFunction(_ versionOfTry: String) throws {
print("try\(versionOfTry)")
throw MyError()
}

try

This method allows us to handle the error at the point where we call the throwing function, or we can allow the error to continue to be passed along to another throwing function (sometimes referred to as bubbling up the error). At some point using this implementation, we have to catch any errors that may be thrown, otherwise our code may not compile or our app could crash.

import Foundation

struct MyError: Error {}

func thisThrowingFunction(_ versionOfTry: String) throws {
print("try\(versionOfTry)")
throw MyError()
}

// Example 1: Handling (catching) the error when our function is called
do {
try thisThrowingFunction("")
} catch {
print("caught error")
}
import Foundation

struct MyError: Error {}

func thisThrowingFunction(_ versionOfTry: String) throws {
print("try\(versionOfTry)")
throw MyError()
}

// Example 2: Passing the error to another function that throws
func anotherFunction() throws {
try thisThrowingFunction("")
}

do {
try anotherFunction()
} catch {
print("caught the error")
}

This method is the right option to use when you want to safely catch the error that was thrown and do something with it. That something could be anything from logging the error, to printing the error, or even displaying a helpful message to the user about the error.

try?

Anytime you see a question mark in Swift, you should think “optional”. This version of try will attempt to try our throwing function, and if our function does in fact throw an error, this version of try will change the value to nil, making the return type of our function be an optional value.

import Foundation

struct MyError: Error {}

func thisThrowingFunction(_ versionOfTry: String) throws {
print("try\(versionOfTry)")
throw MyError()
}

// With this version of try, we aren't forced to catch the error
// and our app won't crash even if an error is thrown from here
// because the output of this function will be changed to nil
try? thisThrowingFunction("?")

// this prints out nil in the terminal
print(try? thisThrowingFunction("?"))

The best place to use this method of try is when you have a function that throws, and you don’t need to handle the error. For example, let’s say we have some other function that we want to return from early if our throwing function throws an error (like in the example below). The error being thrown isn’t important in this example, since all we want to do is see if we need to return early from our other function.

func someOtherFunction() {
guard let _ = try? thisThrowingFunction("?") else {return}

// some other important work
}

try! (or else!)

I’m calling this option try (or else!) because the exclamation point is a force unwrap on the try keyword. Meaning that we don’t have to handle the error like we did with “try” but if we do end up with an error, our app will crash. Even if we attempt to wrap this version of try in a do-catch block, Swift would let us know that the catch block is unreachable. This is because we have told Swift to unwrap this value or (else!) crash. Running the code below, we will see “try!” printed out, then the program crashes because we told Swift to try this function or (else!) crash.

import Foundation

struct MyError: Error {}

func thisThrowingFunction(_ versionOfTry: String) throws {
print("try\(versionOfTry)")
throw MyError()
}

try! thisThrowingFunction("!")

The best time to use this option is when you can guarantee that the function won’t throw an error, possibly because you just assigned a value right before using this method. In that scenario though, you might consider modifying your function so that it doesn’t throw and your app won’t crash. The other place to use this option could be valuable is when you are debugging your app. If you are working in a large complex application, and you are trying to find out why a value isn’t what you expect, maybe you want your app to crash to quickly find out where the problem is. However, in a production app, it is generally best practice to safely unwrap values (not force unwrap them).

If you got value from this article, please consider following me, 👏 clapping for this article, or sharing it to help others more easily find it.

If you have any questions on the topic or know of another way to accomplish the same task, feel free to respond to the post or share it with a friend and get their opinion on it. If you want to learn more about native mobile development, you can check out the other articles I have written here: https://medium.com/@jpmtech. If you want to see apps that have been built with native mobile development, you can check out my apps here: https://jpmtech.io/apps. Thank you for taking the time to check out my work!

--

--