← All topics/Swift language features

Question 6

Error handling: throws and Result

Compare throws/try/catch with Result for modeling failures.

Answer outline

throws, try, and catch are the natural fit for linear flows. Failure interrupts control flow, and the happy path reads top to bottom with no Result to unwrap at each step. Throwing also composes cleanly with async/await, which makes it the default for most Swift error handling.

Result is more useful when failure needs to be treated as a value. Use it when you store an outcome, pass it across a boundary, combine several results by hand, or defer handling to a later point.

Principles

  • Use throws for linear flows, because it keeps the happy path front and center.
  • Use Result when the error needs to be stored or passed as data rather than thrown immediately.
  • Model error types around what the caller needs to handle, and keep implementation details out of them.
  • throws composes naturally with async/await, so prefer it over Result in async code.

Use throws when the caller handles failure as part of normal control flow:

do, try, and catch
func loadProfile() throws -> Profile {
    let data = try Data(contentsOf: profileURL)
    return try JSONDecoder().decode(Profile.self, from: data)
}

do {
    let profile = try loadProfile()
    show(profile)
} catch {
    showError(error)
}

Use Result when you want to store, return, or pass around an outcome without throwing immediately:

Result as a value
func loadProfileResult() -> Result<Profile, Error> {
    do {
        let profile = try loadProfile()
        return .success(profile)
    } catch {
        return .failure(error)
    }
}

let result = loadProfileResult()

switch result {
case .success(let profile):
    show(profile)
case .failure(let error):
    showError(error)
}