Using Swift’s Result Type for Explicit Error Handling (Validation Example)
Learn how Swift’s Result type gives explicit success/failure contracts. See a concrete username‑validation example, interoperability tricks, limits, and common mistakes to avoid.
08 Mar 2026, 12:24 UTC

Why Use Result?
When a function can either succeed with a value or fail with a clear reason, Swift’s Result enum makes that contract explicit. Unlike throwing, the success and failure paths are part of the type, so the caller must handle both, and the compiler will warn if a case is omitted.
Worked Example – Username Validation
Define a custom error that conforms to Error and a function that returns a Result with the validated username or an error.
enum ValidationError: Error, CustomStringConvertible {
case empty
case tooShort
case containsSpace
var description: String {
switch self {
case .empty: return "Username cannot be empty"
case .tooShort: return "Username must be at least 3 characters"
case .containsSpace: return "Username cannot contain spaces"
}
}
}
func validate(username: String) -> Result {
if username.isEmpty { return .failure(.empty) }
if username.count < 3 { return .failure(.tooShort) }
if username.contains(" ") { return .failure(.containsSpace) }
return .success(username)
}
Consuming the Result
Handle both branches with a switch or use functional methods like map and flatMap. The compiler forces you to cover every case.
let input = "Jo"
let result = validate(username: input)
switch result {
case .success(let name):
print("Hello, \(name)!")
case .failure(let error):
print("Error: \(error.description)")
}
Interoperability with Throwing Code
Result does not automatically convert to throwing functions. Wrap a throwing call in the Result initializer or use mapError to translate the error type.
func risky() throws -> Int { /* may throw */ }
let wrapped = Result { try risky() } // Result
// Convert to a custom error type if needed
let customWrapped = wrapped.mapError { _ in ValidationError.empty }
Async/Await
Because Result is synchronous, you must first await a throwing async function and then wrap the outcome.
func fetchNumber() async throws -> Int { /* async throwing */ }
func fetchNumberResult() async -> Result {
do {
let value = try await fetchNumber()
return .success(value)
} catch {
return .failure(error)
}
}
Common Pitfalls
- Omitting a case – the compiler warns if you miss
.successor.failure. Treat warnings as errors in strict builds. - Assuming a
valueproperty –Resulthas no such property. Useswitch,get()(throws), ormapto extract the success value. - Confusing with
Optional– anOptionalonly says “some” or “none”;Resultcarries the failure reason, which is essential for diagnostics. - Using vague error cases – a single generic case loses detail. Define distinct, descriptive cases.
- Copying large payloads –
Resultis a value type; copying it duplicates the associated value. For large data, consider using a reference type orResult<Box<Data>, Error>. - Objective‑C bridging –
Resultdoes not automatically map toNSError. Convert manually:NSError(domain: "com.example", code: 1, userInfo: nil).
Verification Checklist
- Compile the example with Swift 5.5 or later:
- Run the program three times with different inputs:
- Empty string – expect "Error: Username cannot be empty".
- "Jo" – expect "Error: Username must be at least 3 characters".
- "Alice" – expect "Hello, Alice!".
- Check that the compiler emits a warning if you comment out either
.successor.failurecase. - For bridging, create a small helper that converts
ResulttoNSErrorand confirm the error domain and code match your expectations.
swiftc -O main.swift
./main
Limitations
- Not a drop‑in for throwing APIs – you must wrap or map errors.
- Async/await requires manual conversion – there is no built‑in
async Resulttype. - Large payload copy cost – be mindful when the success type is a large struct.
- Objective‑C interop – manual conversion to
NSErroris needed.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.