Swift try, try?, and try! Explained with Examples

Swift uses try, try?, and try! when you call a throwing function. These three forms look similar, but they behave very differently: one requires you to handle errors, one converts errors into nil, and one crashes if an error happens. Understanding the difference is essential for writing safe, predictable Swift code.

Quick answer: Use try when you want to handle or propagate errors normally, try? when a failure should simply become nil, and try! only when you are absolutely sure the call cannot fail at runtime.

Difficulty: Beginner

Helpful to know first: You will understand this better if you know basic Swift functions, optionals, and how if statements and constants work.

1. What Is Swift try, try?, and try!?

In Swift, some functions are marked with throws. That means the function can either return a normal value or stop by throwing an error. When you call one of these functions, Swift makes you be explicit about how you want to deal with the possible failure.

This topic is often confused with optionals and force unwrapping. The key difference is that try? handles a thrown error by returning an optional, while try! does not return an optional just because of the !; instead, it force-assumes success and crashes on failure.

2. Why try Variants Matter

These three forms matter because different failures should be handled in different ways.

For example, if you load a configuration file that must exist for the app to work, you might use normal try and stop with a clear message if it fails. If you are attempting an optional operation, such as parsing a value that may or may not be present, try? may be a good fit. If you are calling something that you know cannot fail in the current context, try! can make code shorter, but it should be rare.

Choosing the wrong form can lead to several problems:

3. Basic Syntax or Core Idea

Defining a throwing function

First, here is a simple throwing function. It returns a username unless the input is empty.

enum ValidationError: Error {
    case emptyName
}

func validatedName(_ name: String) throws -> String {
    if name.isEmpty {
        throw ValidationError.emptyName
    }

    return name
}

This function is marked with throws, so callers must use one of the Swift try forms.

Using plain try

Use plain try when you want to handle the error properly.

do {
    let name = try validatedName("Taylor")
    print(name)
} catch {
    print("Validation failed: \(error)")
}

The do-catch block catches any error thrown by the function.

Using try?

Use try? when you want failure to become an optional value.

let name = try? validatedName("")
print(name as Any)

If the function throws, name becomes nil instead of stopping with an error.

Using try!

Use try! only when failure is impossible or already guaranteed not to happen.

let name = try! validatedName("Morgan")
print(name)

This works if the function succeeds. If it throws, the program stops immediately with a runtime crash.

4. Step-by-Step Examples

Example 1: Handling a thrown error with do-catch

This example shows the standard way to call a throwing function and respond to failure.

enum PasswordError: Error {
    case tooShort
}

func checkPassword(_ password: String) throws -> String {
    if password.count < 8 {
        throw PasswordError.tooShort
    }

    return "Accepted"
}

do {
    let result = try checkPassword("secret123")
    print(result)
} catch {
    print("Password check failed: \(error)")
}

Use this style when the reason for failure matters and you want to keep that information.

Example 2: Using try? for optional parsing

Sometimes a failure is not exceptional. In that case, turning a thrown error into nil makes the code simpler.

enum ParseError: Error {
    case invalidNumber
}

func parseWholeNumber(_ text: String) throws -> Int {
    guard let number = Int(text) else {
        throw ParseError.invalidNumber
    }

    return number
}

let age = try? parseWholeNumber("42")
let badAge = try? parseWholeNumber("forty-two")

print(age as Any)
print(badAge as Any)

The first result is an optional containing 42. The second result is nil. This is useful when you only care whether the operation succeeded.

Example 3: Using try! when success is guaranteed

This example shows the narrow case where try! can be acceptable.

enum TextError: Error {
    case empty
}

func firstCharacter(of text: String) throws -> Character {
    guard let character = text.first else {
        throw TextError.empty
    }

    return character
}

let letter = try! firstCharacter(of: "Swift")
print(letter)

This works because the string literal is clearly non-empty. The risk is low here, but in general try! should be used sparingly.

Example 4: Propagating errors from another throwing function

You do not always handle errors immediately. Sometimes your function should pass them upward.

enum LoginError: Error {
    case missingUsername
}

func validateUsername(_ username: String) throws -> String {
    if username.isEmpty {
        throw LoginError.missingUsername
    }

    return username
}

func normalizedUsername(from input: String) throws -> String {
    let username = try validateUsername(input)
    return username.lowercased()
}

do {
    let name = try normalizedUsername(from: "Admin")
    print(name)
} catch {
    print("Login problem: \(error)")
}

Here, the inner function uses plain try because it wants to pass the error upward rather than converting or force-assuming success.

5. Practical Use Cases

6. Common Mistakes

Mistake 1: Calling a throwing function without try

A throwing function must be called with one of Swift's try forms. Beginners often forget this when they first see throws.

Problem: Swift will report an error like Call can throw, but it is not marked with 'try' and the error is not handled because the code ignores the function's failure path.

enum FileError: Error {
    case missing
}

func loadFileName() throws -> String {
    throw FileError.missing
}

let name = loadFileName()

Fix: Add try and either catch the error or propagate it.

do {
    let name = try loadFileName()
    print(name)
} catch {
    print("Could not load file name: \(error)")
}

The corrected version works because it explicitly acknowledges that the function can fail.

Mistake 2: Using try? when the error information matters

try? is convenient, but it silently discards the actual error. That can make debugging and user feedback much harder.

Problem: This code turns every failure into nil, so you cannot tell whether the problem was missing data, invalid input, or something else.

enum AccountError: Error {
    case locked
    case missingUser
}

func fetchAccountName(for id: Int) throws -> String {
    throw AccountError.locked
}

let name = try? fetchAccountName(for: 42)
print(name as Any)

Fix: Use plain try with do-catch when the cause of failure should be preserved.

do {
    let name = try fetchAccountName(for: 42)
    print(name)
} catch AccountError.locked {
    print("The account is locked.")
} catch AccountError.missingUser {
    print("No user was found.")
} catch {
    print("Unexpected error: \(error)")
}

The corrected version works because it keeps the error information available for specific handling.

Mistake 3: Using try! when failure is possible

try! is the riskiest form. It looks short and convenient, but it will crash if the function throws.

Problem: If the input is invalid, this code causes a runtime trap instead of handling the failure safely.

enum NumberError: Error {
    case negative
}

func squareRoot(of value: Int) throws -> Double {
    if value < 0 {
        throw NumberError.negative
    }

    return Double(value).squareRoot()
}

let result = try! squareRoot(of: -9)
print(result)

Fix: Use plain try and catch the error, or use try? if losing the error details is acceptable.

do {
    let result = try squareRoot(of: -9)
    print(result)
} catch {
    print("Cannot calculate square root: \(error)")
}

The corrected version works because it handles the error path instead of force-assuming success.

7. Best Practices

Prefer try as the default choice

In most real applications, errors are meaningful. Starting with plain try keeps the code honest and preserves error details.

do {
    let username = try validatedName("Avery")
    print(username)
} catch {
    print("Validation error: \(error)")
}

This approach is easier to maintain because future failures remain visible and debuggable.

Use try? only for truly optional results

If failure is expected and should simply mean “no value,” try? is a clean option.

let number = try? parseWholeNumber("100")

if let number = number {
    print("Parsed: \(number)")
} else {
    print("No valid number provided.")
}

This is a good fit because the code only needs success or absence, not detailed error reasons.

Avoid try! in production paths unless failure is impossible

Even if try! looks safe today, later code changes can make it unsafe. Keep it for tightly controlled situations.

// Acceptable only because the input is fully known here.
let letter = try! firstCharacter(of: "Hello")
print(letter)

Use this style only when the failure condition is impossible in practice and obvious to future readers.

8. Limitations and Edge Cases

9. Swift try vs try? vs try!

Because these three forms are commonly confused, it helps to compare them directly.

FormWhat happens on successWhat happens on failureTypical use
tryReturns the normal valueThrows an error that must be caught or propagatedDefault choice when error details matter
try?Returns an optional containing the valueReturns nilOptional or best-effort operations
try!Returns the normal valueCrashes the programRare cases where failure is impossible

A simple rule is: start with try, switch to try? only when nil is the right failure result, and use try! only when you can prove the call will never throw.

10. Practical Mini Project

This mini project validates a promo code entered by the user. It shows all three forms in a small, complete example.

enum PromoCodeError: Error {
    case emptyCode
    case invalidCode
}

func validatePromoCode(_ code: String) throws -> String {
    if code.isEmpty {
        throw PromoCodeError.emptyCode
    }

    let allowedCodes = ["SAVE10", "FREESHIP"]

    guard allowedCodes.contains(code) else {
        throw PromoCodeError.invalidCode
    }

    return code
}

// 1. Standard error handling with try
do {
    let validCode = try validatePromoCode("SAVE10")
    print("Accepted code: \(validCode)")
} catch {
    print("Promo code error: \(error)")
}

// 2. Optional handling with try?
let optionalCode = try? validatePromoCode("BADCODE")
print("Optional result: \(optionalCode as Any)")

// 3. Forced success with try!
let knownCode = try! validatePromoCode("FREESHIP")
print("Known valid code: \(knownCode)")

This example shows the three variants side by side. In a real app, the first version is usually the safest, the second is useful for optional flows, and the third should be limited to guaranteed-success inputs.

11. Key Points

12. Practice Exercise

Build a throwing function called positiveNumber(from:) that:

Expected output: One successful result and one optional nil for invalid input.

Hint: Create an enum that conforms to Error and use guard statements.

enum PositiveNumberError: Error {
    case notANumber
    case notPositive
}

func positiveNumber(from text: String) throws -> Int {
    guard let number = Int(text) else {
        throw PositiveNumberError.notANumber
    }

    guard number > 0 else {
        throw PositiveNumberError.notPositive
    }

    return number
}

do {
    let value = try positiveNumber(from: "25")
    print("Valid number: \(value)")
} catch {
    print("Error: \(error)")
}

let optionalValue = try? positiveNumber(from: "-5")
print("Optional result: \(optionalValue as Any)")

13. Final Summary

Swift's try variants let you choose how a throwing function should behave at the call site. Plain try is the normal and safest default because it keeps errors visible. try? is useful when failure should quietly become nil. try! is the most dangerous option because it crashes if anything goes wrong.

If you remember one rule, make it this: use try first, use try? when an optional result is the right model, and use try! only when failure is truly impossible. A good next step is to learn deeper Swift error handling patterns such as custom error types, do-catch matching, and rethrowing functions.