Swift Error Handling

Swift models recoverable failures with the Error protocol and the throw, try, and catch keywords, forcing you to explicitly acknowledge when a call might fail.

Defining Errors

Any type conforming to the empty Error protocol can be thrown, but enums are the most common choice because they let you group related failure cases together and switch over them exhaustively in a catch block.

Defining a Custom Error

enum ValidationError: Error {
    case tooShort
    case tooLong
    case containsInvalidCharacters
}

func validate(username: String) throws -> String {
    if username.count < 3 {
        throw ValidationError.tooShort
    }
    if username.count > 20 {
        throw ValidationError.tooLong
    }
    guard username.allSatisfy({ $0.isLetter || $0.isNumber }) else {
        throw ValidationError.containsInvalidCharacters
    }
    return username
}

Calling Throwing Functions

Calling a throwing function requires the try keyword, and try itself must appear inside a do-catch block or another throwing function. A do-catch block can have multiple catch clauses that match specific error cases, plus a catch-all clause at the end.

Handling Errors With do-catch

let candidates = ["ab", "validUser42", "bad*name"]

for candidate in candidates {
    do {
        let approved = try validate(username: candidate)
        print("\(approved) is valid")
    } catch ValidationError.tooShort {
        print("\(candidate) is too short")
    } catch ValidationError.tooLong {
        print("\(candidate) is too long")
    } catch {
        print("\(candidate) failed: \(error)")
    }
}

try?, try!, and Cleanup With defer

Not every call site needs full error inspection. try? converts a thrown error into an optional nil, while try! asserts the call cannot fail and crashes at runtime if it does. defer schedules cleanup code that runs when the current scope exits, whether it exits normally or by throwing.

Optional and Forced Try, Plus defer

// try? converts a thrown error into nil
let maybeValid = try? validate(username: "ok")
print(maybeValid ?? "invalid") // invalid (too short)

// try! asserts the call cannot fail — crashes if it does
let forced = try! validate(username: "safeUser1")
print(forced) // safeUser1

func processFile() throws {
    print("Opening file")
    defer { print("Closing file") } // always runs, even on throw
    throw ValidationError.tooLong
}

try? processFile()
// Prints: Opening file
//         Closing file
  • try — propagate the error to the caller; requires a throwing function or a do-catch
  • try? — turn any thrown error into nil, useful when you only care about success or failure
  • try! — assert the call will always succeed; crashes the app if it throws
  • do-catch — the only form that lets you inspect and react to the specific error thrown
FormOn SuccessOn FailureUse When
tryReturns the valuePropagates the errorYou handle it in a do-catch or rethrow it
try?Returns Optional(value)Returns nilYou only care whether it worked
try!Returns the valueCrashes at runtimeYou are certain it cannot fail
Note: Custom error enums can carry associated values, such as case invalidAge(Int), so a catch clause can extract details about exactly what went wrong.
Note: Never use try! for errors that depend on external input like network responses or user-selected files — an unexpected failure will crash your app immediately.

Exercise: Swift Error Handling

What must a type do to be usable as a Swift error?