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
- • What Result<Success, Failure> is (a two-case enum)
- • Build outcomes with .success and .failure
- • Handle a Result with a switch
- • Bridge to throwing code with get()
- • Transform success values with map
- • Choose Result vs a throwing function
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 30Pro Tips
- 💡 Use Result for stored/deferred outcomes — callbacks, caches, queued work — and throwing for immediate handling.
- 💡 Result { try f() } converts a throwing call into a Result in one line.
- 💡 map / flatMap transform the success without touching a failure.
- 💡 Reach for get() to slot a Result into existing try/catch code.
Common Errors (and the fix)
- "Type ... does not conform to protocol 'Error'": the Failure type must conform to Error. Make your error enum : Error.
- "Call can throw but is not marked with 'try'": get() throws — write try result.get() inside do/catch.
- Switch "must be exhaustive": handle both .success and .failure (or add default).
- Forcing with try! on a failure: that crashes. Use a switch or do/catch instead.
📋 Quick Reference
| Task | Code |
|---|---|
| Declare | Result<Int, MyError> |
| Wrap value | .success(42) |
| Wrap error | .failure(.someError) |
| Handle | switch r { case .success(let v): … } |
| To try/catch | let v = try r.get() |
| From throwing | Result { try f() } |
| Transform | r.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!
- ✅ Result<Success, Failure> is a two-case enum (.success / .failure)
- ✅ The Failure type conforms to Error
- ✅ Handle it with a switch, or bridge to throwing with try get()
- ✅ map transforms success; failure passes through untouched
- ✅ Prefer Result for stored/callback outcomes, throwing for immediate handling
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
- Previous: Codable & JSON
- Next: SwiftUI Views & the body Property — The View protocol, the body property, and composing Text, Image and Button
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.