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| Form | Behaviour | Use when |
|---|---|---|
try | Propagates to the caller | Inside a throws function |
try? | Converts failure to nil | Failure is not interesting, or has a default |
try! | Crashes on failure | Only in tests and hard-coded literals |
Result | Value holding success or failure | Storing or passing an outcome, callback APIs |
defer | Runs on every exit path | Releasing 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.Related
Structs, classes and protocols Syntax and optionals
Last refreshed 2026-09-18.