Closures and error handling

Closure syntax and capture semantics, throwing functions with do-catch, and combining both with async and await.

Closures

A closure is a function you can pass around. Swift gives it compact syntax — trailing closures, shorthand argument names and implicit returns — which makes the longer forms easy to forget when you need them.

let numbers = [3, 1, 2]

// full form
let sorted1 = numbers.sorted(by: { (a: Int, b: Int) -> Bool in a < b })
// shorthand argument names, implicit return
let sorted2 = numbers.sorted { $0 < $1 }

var completions: [() -> Void] = []

func loadRemote(with completion: @escaping (Result<String, Error>) -> Void) {
    completions.append(completion)          // escaping: outlives the call
}

class Loader {
    var onFinish: (() -> Void)?

    func start() {
        onFinish = { [weak self] in         // break the retain cycle
            guard let self else { return }
            print("finished")
        }
    }
}
  • A closure is non-escaping by default; mark a stored or asynchronously-called one @escaping.
  • Captured variables are captured by reference; [value] in the capture list takes a snapshot instead.
  • [weak self] is optional and must be unwrapped; [unowned self] is not optional but crashes if the object is gone — use it only when the object outlives the closure.

Throwing and catching

Swift distinguishes recoverable errors from programmer mistakes. A function declares throws and reports failure with throw; the caller must handle it, propagate it, or explicitly convert it.

enum ParseError: Error, Equatable {
    case empty
    case notANumber(String)
}

func parse(_ text: String) throws -> Int {
    let trimmed = text.trimmingCharacters(in: .whitespaces)
    guard !trimmed.isEmpty else { throw ParseError.empty }
    guard let value = Int(trimmed) else { throw ParseError.notANumber(trimmed) }
    return value
}

do {
    let n = try parse(" 42 ")
    print(n)
} catch ParseError.empty {
    print("nothing to parse")
} catch let ParseError.notANumber(raw) {
    print("bad input: " + raw)
} catch {
    print("unexpected: " + String(describing: error))
}

let lenient = try? parse("abc")      // nil instead of throwing
let strict = try! parse("7")         // crashes on failure - avoid in production
FormBehaviourUse when
tryPropagates to the callerInside a throws function
try?Converts failure to nilFailure is not interesting, or has a default
try!Crashes on failureOnly in tests and hard-coded literals
ResultValue holding success or failureStoring or passing an outcome, callback APIs
deferRuns on every exit pathReleasing a lock or cleaning up a temporary file

Errors with async and await

func fetchUser(id: String) async throws -> User {
    let (data, response) = try await URLSession.shared.data(from: url)
    guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
        throw ParseError.empty
    }
    return try JSONDecoder().decode(User.self, from: data)
}

func load() async {
    do {
        let user = try await fetchUser(id: "42")
        print(user.email)
    } catch is CancellationError {
        return                                  // the task was cancelled; not an error
    } catch {
        print("failed: " + String(describing: error))
    }
}
⚠️
Cancellation in Swift concurrency is cooperative: await points throw CancellationError, but a tight synchronous loop ignores cancellation entirely. Check with Task.isCancelled or call try Task.checkCancellation() inside long work.

FAQ

When do I need @escaping?
When the closure is stored or called after the function returns — a completion handler, a property, or a task started inside the function. Inout and non-escaping closures do not need it.
Should I use Result or errors?
Prefer throws for synchronous and async calls where the caller handles failure right there. Use Result when the outcome must be stored or passed through an API that cannot throw, such as a Combine publisher.

Structs, classes and protocols Syntax and optionals

Last refreshed 2026-09-18.