Using Swift’s Result Type for Explicit Error Handling
Learn how Swift’s Result type gives you typed errors without throwing, with a practical network‑request example and guidance on when to use it.
14 Oct 2025, 13:23 UTC

The Problem: Silent Errors in Async Completion Handlers
When you write a networking layer that calls a completion handler, errors often arrive as an optional Error? or are swallowed entirely. This forces callers to check for nil and then inspect the error’s type, which can hide mistakes and makes it easy to forget a failure case.
Thesis: Result Gives You Typed Errors Without Throwing
Swift’s Result type, introduced in Swift 5, models a computation that can either succeed with a value of type Success or fail with a value of type Failure. By returning Result<Value, Error> you keep the error information in the normal return path, avoid do‑try‑catch blocks, and enable functional combinators like map and flatMap.
How Result Works
Result is defined as:
enum Result<Success, Failure> where Failure : Error { case success(Success) case failure(Failure) }You can create a
Resultwith the initializers.success(value)and.failure(error). To extract the value you switch on the enum or use helper methods:
maptransforms the success value while leaving a failure untouched.flatMaplets you chain operations that themselves return aResult.get()throws the underlying error, useful when you need to interoperate with throwing APIs.
Worked Example: Network Request Wrapper
Consider a simple wrapper around URLSession that returns a Result instead of using throwing functions:
import Foundation func fetchJSON<T: Decodable>( from url: URL, decoder: JSONDecoder = JSONDecoder(), completion: @escaping (Result<T, Error>) -> Void ) { let task = URLSession.shared.dataTask(with: url) { data, response, error in if let error = error { completion(.failure(error)) return } guard let data = data else { completion(.failure(URLError(.badServerResponse))) return } do { let decoded = try decoder.decode(T.self, from: data) completion(.success(decoded)) } catch { completion(.failure(error)) } } task.resume() } // Usage let url = URL(string: "https://api.example.com/user/42")! struct User: Decodable { let id: Int; let name: String } fetchJSON(from: url) { result in switch result { case .success(let user): print("Received user: " + user.name) case .failure(let err): print("Failed: " + err.localizedDescription) } }The caller never needs a
do‑try‑catchblock; pattern matching on theResultmakes the success and failure paths explicit. If you later want to use the value in a throwing context, you can calltry result.get().Trade‑off: Verbosity vs. Clarity
For simple synchronous functions that only ever fail with a single error type,
throwsis often more concise:func parseInt(_ s: String) throws -> Int { guard let value = Int(s) else { throw ParsingError.invalidInput } return value }Wrapping every such function in
Resultadds boilerplate without gaining much. UseResultwhen you need to:
- Pass errors through asynchronous completion handlers.
- Combine multiple fallible steps with
map/flatMapwithout nestingdo‑try‑catch. - Preserve error types in generic APIs where the caller may want to inspect the failure case.
Actionable Takeaway
Start by replacing one asynchronous completion‑handler API that currently returns an optional error with a function that returns Result<Value, Error>. Test both success and failure branches in a Swift Playground:
let expectation = XCTExpectation(description: "fetch") fetchJSON(from: url) { result in switch result { case .success: XCTAssertTrue(true) case .failure: XCTAssertTrue(true) } expectation.fulfill() } wait(for: [expectation], timeout: 5)If the test passes for both cases, you have verified that the error information travels correctly. Adopt
Resultwherever asynchronous error propagation matters, and keepthrowsfor straightforward synchronous code.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.