Swift Result Type (Result<Success, Failure>): Complete Guide

The Result type is Swift’s standard way to represent an operation that either succeeds with a value or fails with an error. It is especially useful when you want to return success and failure information from a function, callback, or asynchronous API in a form that is easy to switch over and compose.

Quick answer: Result<Success, Failure> is an enum with two cases: .success(Success) and .failure(Failure). Use it when you want a typed, explicit way to return either a value or an error instead of throwing or using optional values.

Difficulty: Beginner to Intermediate

You'll understand this better if you know: basic Swift enums, error handling with throw and catch, and how functions return values.

1. What Is Result<Success, Failure>?

Result is a generic enum in the Swift standard library designed to model the outcome of an operation. It wraps either a successful value or a failure value, and both sides are part of the type signature.

This makes Result a safer alternative to “return a value or hope the caller notices the error somehow.”

2. Why Result Matters

Result matters because many Swift APIs need to communicate success and failure without immediately stopping execution. It is common in networking, file loading, parsing, and asynchronous callbacks.

Compared with using an optional, Result tells you why something failed. Compared with a thrown error, it can be easier to pass through closures, store, or transform later.

Use Result when you want:

3. Basic Syntax or Core Idea

The generic form is Result<Success, Failure>. Swift often infers the generic types, so you may also see just Result in context.

The two cases

Here is the minimal shape of a result value:

enum Result<Success, Failure : Error> {
    case success(Success)
    case failure(Failure)
}

In real Swift code, you do not define this yourself; it already exists in the standard library.

Creating a result value

You create a successful result with .success and a failing result with .failure.

let successResult = Result<String, Error>.success("Downloaded")
let failureResult = Result<String, Error>.failure(URLError(.notConnectedToInternet))

This gives you a value you can return, store, inspect, or transform later.

4. Step-by-Step Examples

Example 1: Returning a result from a function

Here is a simple parser that either returns a number or a custom error.

enum ParseError: Error {
    case emptyInput
    case invalidNumber
}

func parseScore(_ text: String) -> Result<Int, ParseError> {
    guard !text.isEmpty else {
        return .failure(.emptyInput)
    }

    guard let score = Int(text) else {
        return .failure(.invalidNumber)
    }

    return .success(score)
}

This pattern makes the caller handle both the parsed value and the failure reason.

Example 2: Switching over the result

Callers usually use switch to handle both cases explicitly.

let result = parseScore("42")

switch result {
case .success(let value):
    print("Parsed score:", value)
case .failure(let error):
    print("Could not parse score:", error)
}

The compiler forces you to account for both paths, which is one of the main advantages of Result.

Example 3: Using map to transform success values

If the operation succeeds, you can transform the wrapped value without changing the failure case.

let parsed = parseScore("7")
let doubled = parsed.map { value in
    value * 2
}

map only changes the success value. If the result is already a failure, it stays a failure.

Example 4: Converting a result into a thrown error

Sometimes you receive a result but want to integrate with throwing code.

func scoreValue(from text: String) throws -> Int {
    switch parseScore(text) {
    case .success(let value):
        return value
    case .failure(let error):
        throw error
    }
}

This is a common bridge when moving between callback-based APIs and throwing APIs.

5. Practical Use Cases

Result is most useful when you want to keep error information attached to the operation outcome.

It is less useful when failure is impossible, when a simple optional is enough, or when the caller should not proceed at all after an error.

6. Common Mistakes

Mistake 1: Using Result when an optional is enough

Sometimes developers use Result just to say “maybe there is a value.” That adds unnecessary complexity when there is no meaningful error to report.

Problem: If the only thing you can say is that the value is missing, wrapping it in Result makes the API harder to use without adding useful information.

func nickname(for name: String) -> Result<String, Error> {
    if name.isEmpty {
        return .failure(NSError(domain: "App", code: 1))
    }
    return .success(name)
}

Fix: Use an optional when you only need to represent presence or absence.

func nickname(for name: String) -> String? {
    guard !name.isEmpty else {
        return nil
    }
    return name
}

The corrected version is easier to read because it uses the simplest type that matches the problem.

Mistake 2: Ignoring the failure case

Some code only looks at the success path and forgets that a result can fail. This usually happens when developers try to extract the value too early.

Problem: Accessing only the success side without handling the failure leads to incomplete logic and often to compiler complaints when the result is not fully covered.

let result = parseScore("abc")

if case .success(let value) = result {
    print(value)
}

Fix: Handle both cases with switch or provide an else branch.

switch parseScore("abc") {
case .success(let value):
    print("Value:", value)
case .failure(let error):
    print("Error:", error)
}

The corrected version works because it does not assume success is the only possible outcome.

Mistake 3: Using a non-Error failure type

The failure type must conform to Error. If it does not, Swift will reject the declaration.

Problem: A failure type like String does not satisfy the generic constraint, so the compiler cannot treat it as a valid Result failure.

let badResult: Result<Int, String> = .failure("Not allowed")

Fix: Define an error type that conforms to Error, or use an existing error type such as Error or URLError.

enum LoadError: Error {
    case message(String)
}

let goodResult: Result<Int, LoadError> = .failure(.message("Not allowed"))

The corrected version compiles because the failure type now meets Swift’s requirement.

7. Best Practices

Practice 1: Use precise failure types

A specific error type makes your API clearer than a generic Error when you know the likely failure cases.

enum LoginError: Error {
    case emptyUsername
    case invalidPassword
}

This helps callers understand and handle failures more accurately.

Practice 2: Prefer switch when both outcomes matter

switch makes it obvious that both cases are handled.

switch parseScore("8") {
case .success(let value):
    print(value)
case .failure(let error):
    print(error)
}

This reads better than forcing the value out of the result too early.

Practice 3: Use result helpers for transformation

Methods like map, mapError, and flatMap keep pipelines clean.

let message = parseScore("12")
    .map { value in "Score: \(value)" }
    .mapError { error in "Parse error: \(error)" }

This keeps success and failure handling in one chain instead of branching repeatedly.

8. Limitations and Edge Cases

A common “not working” scenario is assuming that Result itself prints a readable message. It only stores the outcome; formatting is up to your code.

9. Practical Mini Project

Let’s build a tiny account lookup service that returns a result instead of throwing. This example shows how Result keeps success and failure handling explicit.

struct User {
    let id: Int
    let name: String
}

enum LookupError: Error {
    case notFound
    case invalidID
}

func lookupUser(id text: String) -> Result<User, LookupError> {
    guard let id = Int(text) else {
        return .failure(.invalidID)
    }

    if id == 1 {
        return .success(User(id: 1, name: "Ava"))
    } else {
        return .failure(.notFound)
    }
}

let lookup = lookupUser(id: "1")

switch lookup {
case .success(let user):
    print("Welcome, \(user.name)")
case .failure(let error):
    switch error {
    case .notFound:
        print("User not found")
    case .invalidID:
        print("Please enter a numeric ID")
    }
}

This mini project shows a typical pattern: parse input, return a typed success or failure, and handle both outcomes clearly at the call site.

10. Key Points

11. Practice Exercise

Expected output: If the denominator is zero, the program should report a division error. Otherwise, it should print the calculated quotient.

Hint: Check the denominator before dividing and return .failure early when it is zero.

enum DivisionError: Error {
    case divisionByZero
}

func divide(_ lhs: Double, _ rhs: Double) -> Result<Double, DivisionError> {
    guard rhs != 0 else {
        return .failure(.divisionByZero)
    }

    return .success(lhs / rhs)
}

switch divide(10, 2) {
case .success(let value):
    print("Quotient:", value)
case .failure(let error):
    print("Division failed:", error)
}

12. Final Summary

Result<Success, Failure> is Swift’s built-in way to represent an operation that either produced a value or failed with an error. It is especially helpful when you want the outcome to stay explicit, type-safe, and easy to handle in callbacks or transformation pipelines.

Use it when the caller needs to know both the success value and the reason for failure. Prefer a simple optional when failure is not meaningful, and prefer throwing when synchronous code reads more naturally with try and catch.

Once you are comfortable switching over Result, the next step is to learn how to combine it with map, flatMap, and custom error types in real APIs.