The Result Type

Reviewed & published by Brayan K

Capture an operation's outcome — a value or an error — as a single value with Result<Success, Failure>. Learn its .success/.failure cases, the get() bridge to try/catch, and when to choose it over a throwing function.

Part of the free Swift course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1️⃣ Result Basics — .success and .failure

Result<Success, Failure> is an enum with two cases. Build a value with .success(...) or .failure(...) (the failure type must conform to Error), then unwrap it with a switch.

// Result<Success, Failure> is an enum with exactly two cases.
// Failure must conform to Error. It bundles an outcome as a VALUE.

enum DivideError: Error {
    case divisionByZero
}

func divide(_ a: Int, by b: Int) -> Result<Int, DivideError> {
    if b == 0 {
        return .failure(.divisionByZero)   // wrap the error
    }
    return .success(a / b)                  // wrap the value
}

// Handle it with a switch over the two cases.
let r = divide(10, by: 2)
switch r {
case .success(let value):
    print("Got \(value)")        // Got 5
case .failure(let error):
    print("Failed: \(error)")
}

let bad = divide(10, by: 0)
switch bad {
case .success(let value):
    print("Got \(value)")
case .failure(let error):
    print("Failed: \(error)")    // Failed: divisionByZero
}

2️⃣ Bridging to try/catch with get()

get() returns the success value or throws the contained error — so you can drop a Result straight into do/try/catch code.

// get() bridges Result back to try/catch: it returns the success value,
// or THROWS the contained error if it's a failure.

enum ParseError: Error { case notANumber }

func parse(_ s: String) -> Result<Int, ParseError> {
    if let n = Int(s) {
        return .success(n)
    }
    return .failure(.notANumber)
}

let ok = parse("42")
let bad = parse("hello")

// Use try get() inside do/catch.
do {
    let n = try ok.get()
    print("Parsed: \(n)")        // Parsed: 42
} catch {
    print("Error: \(error)")
}

do {
    let n = try bad.get()
    print("Parsed: \(n)")
} catch {
    print("Error: \(error)")     // Error: notANumber
}

3️⃣ Result vs Throwing

A throwing function surfaces the error immediately at the call site; a Result packages the outcome as a value you can store or pass to a callback that can't throw. They convert easily: Result { try ... } wraps a throwing call, and map transforms the success value.

// Result vs throwing — two ways to model failure.

enum LoadError: Error { case empty }

// THROWING: the error surfaces immediately at the call site (try/catch).
func loadThrowing(_ s: String) throws -> String {
    if s.isEmpty { throw LoadError.empty }
    return "Loaded: \(s)"
}

// RESULT: the outcome is a VALUE you can store, pass around, or return
// from a callback that can't throw.
func loadResult(_ s: String) -> Result<String, LoadError> {
    s.isEmpty ? .failure(.empty) : .success("Loaded: \(s)")
}

// Bridge between them easily:
// 1) Turn a throwing call into a Result:
let asResult = Result { try loadThrowing("file.txt") }
// 2) map transforms success, leaving failure untouched:
let lengths = loadResult("data").map { $0.count }
print((try? asResult.get()) ?? "n/a")   // Loaded: file.txt
print(lengths)                          // success(12)

Your turn. Fill in the failure and success case constructors.

// 🎯 YOUR TURN — fill in each ___ then run it on SwiftFiddle.

enum AgeError: Error { case negative }

func makeAge(_ n: Int) -> Result<Int, AgeError> {
    if n < 0 {
        return .___( .negative)     // 👉 the FAILURE case constructor
    }
    return .___(n)                  // 👉 the SUCCESS case constructor
}

let r = makeAge(30)
switch r {
case .success(let age):
    print("Age is \(age)")          // Age is 30
case .failure(let error):
    print("Bad age: \(error)")
}

// ✅ Expected output:
//    Age is 30

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

TaskCode
DeclareResult<Int, MyError>
Wrap value.success(42)
Wrap error.failure(.someError)
Handleswitch r { case .success(let v): … }
To try/catchlet v = try r.get()
From throwingResult { try f() }
Transformr.map { $0 * 2 }

Mini-Challenge: Validated Username

No blanks this time — just a brief and an outline. Return a Result from a validator and handle both cases.

// 🎯 MINI-CHALLENGE: validated username
// 1. Make  enum NameError: Error { case tooShort }
// 2. Write  func validate(_ name: String) -> Result<String, NameError>
//    returning .failure(.tooShort) when name.count < 3, else .success(name).
// 3. Call it with "Al" and "Alice".
// 4. Handle each with a switch, printing "OK: <name>" or "Error: <error>".
// 5. BONUS: use try validate("Bo").get() inside a do/catch.
//
// ✅ Expected:
//    Error: tooShort
//    OK: Alice

// your code here

🎉 Lesson Complete!

Practice quiz

What is Result in Swift?

  • A networking class
  • A type of optional
  • An enum with two cases representing success or failure
  • A UI view

Answer: An enum with two cases representing success or failure. Result is a generic enum with .success and .failure cases that bundles an outcome and its possible error.

What are the two cases of Result<Success, Failure>?

  • .success and .failure
  • .ok and .error
  • .value and .nil
  • .some and .none

Answer: .success and .failure. Result has exactly two cases: .success(Success) and .failure(Failure).

What constraint does the Failure type of Result have?

  • It must be a class
  • It must be an Int
  • It has no constraint
  • It must conform to the Error protocol

Answer: It must conform to the Error protocol. Result<Success, Failure> requires Failure: Error, so the failure case carries an Error value.

How do you wrap a successful Int value of 42 in a Result?

  • Result(42)
  • .success(42)
  • .value(42)
  • .ok(42)

Answer: .success(42). You construct a success with .success(42).

What does calling get() on a Result do?

  • Returns the success value or THROWS the failure's error
  • Always returns nil
  • Returns a Bool
  • Crashes on success

Answer: Returns the success value or THROWS the failure's error. get() returns the wrapped success value, or throws the contained error if it's a failure — bridging Result back to try/catch.

How do you typically handle a Result value?

  • With a for loop
  • With optional chaining
  • With a switch over .success(let value) and .failure(let error)
  • With guard only

Answer: With a switch over .success(let value) and .failure(let error). A switch with .success(let value) and .failure(let error) cases is the idiomatic way to handle a Result.

How does Result differ from a throwing function?

  • They are identical
  • Result is a value you can store and pass around; throwing happens immediately at the call site
  • Result cannot represent errors
  • Throwing functions cannot fail

Answer: Result is a value you can store and pass around; throwing happens immediately at the call site. Result captures the outcome as a value (great for callbacks/storage), while throwing surfaces the error right where it's called.

Why is Result especially handy in completion handlers / callbacks?

  • It is faster
  • It avoids optionals entirely
  • It is required by SwiftUI
  • A completion handler can't throw, so Result lets it deliver either a value or an error as one parameter

Answer: A completion handler can't throw, so Result lets it deliver either a value or an error as one parameter. Asynchronous callbacks can't use try/catch, so passing a Result cleanly carries success or failure in a single argument.

What does Result's map method do?

  • Filters the failure
  • Transforms the success value while leaving a failure untouched
  • Converts to an optional
  • Throws the error

Answer: Transforms the success value while leaving a failure untouched. map applies a transform to the success value and passes any failure through unchanged.

Given 'let r: Result<Int, Error>', how do you safely convert it to try/catch style?

  • r.value
  • r!
  • do { let v = try r.get() } catch { … }
  • r as? Int

Answer: do { let v = try r.get() } catch { … }. try r.get() inside a do/catch returns the value or throws the error, bridging Result to throwing code.

Continue this course

Frequently asked questions

What is the Result type in Swift?

Result<Success, Failure> is a generic enum from the standard library with two cases: .success, which carries a value of the Success type, and .failure, which carries an error of the Failure type (Failure must conform to Error). It lets you represent the outcome of an operation — either a value or an error — as a single value you can store, return, and pass around, rather than throwing immediately.

How do I create and handle a Result?

You create one with .success(value) or .failure(error). To handle it, switch over the two cases: 'case .success(let value):' uses the value, and 'case .failure(let error):' handles the error. You can also call .get(), which returns the success value or throws the failure's error, letting you slot Result into ordinary do/try/catch code.

What does the get() method do?

get() bridges Result back to Swift's throwing world. Calling try result.get() returns the wrapped success value if the Result is a success, or throws the contained error if it's a failure. This is handy when you have a Result but the surrounding code is written with try/catch — you get the value out, or the error propagates as a thrown error.

When should I use Result instead of a throwing function?

Use a throwing function when the error should surface right at the call site and the caller will handle it immediately with try/catch — that's the idiomatic default for synchronous code. Reach for Result when you need the outcome as a value: storing it for later, passing it through a completion handler or callback that can't throw, or threading it through asynchronous code. Result and throwing are interchangeable — Result { try something() } wraps a throwing call, and get() unwraps a Result back into a throw.

Why is Result so common in completion handlers?

A completion handler is a closure called when an asynchronous task finishes, and closures can't use try/catch to report failure to their caller. Before async/await, the clean solution was a single parameter of type Result<Value, Error>: the handler receives either .success(value) or .failure(error), so the caller switches on one argument instead of juggling separate value and error parameters that could both be nil or both be set. Modern code often uses async/await instead, but you'll still see Result widely in callback-based APIs.