Skip to content

Modeling Errors in Swift (SwiftUI)

A SwiftUI guide for coding agents. Also covers error enum design, throw catch patterns, when to throw vs return nil, reduce nested do catch, error handling api design.

How to design the error surface of an API, so client code does not drown in nested do-catch blocks. An enum conforming to Error fits mutually exclusive failure cases, but the real skill is deciding which failures deserve to be errors at all. The three principles come from John Ousterhout's "A Philosophy of Software Design".

The Smell: One Error Case Per Situation

A cache that throws for every situation forces its callers into nested do-catch pyramids.

actor InMemoryCache<Key: Hashable & Codable, Value: Codable> {
  enum ErrorKind: Error {
    case noValue(Key)
    case outOfMemory(availableBytes: Int)
  }

  func get(for key: Key) throws -> Value {
    guard contains(key: key) else { throw ErrorKind.noValue(key) }
    // fetch and return value here
  }
}

The caller must catch noValue, then fetch remotely, then catch outOfMemory from the write: three nested blocks for one code path.

Define Errors Out of Existence

A missing cache value is not an exceptional situation, it is the normal miss case. Return an optional instead of throwing, and the caller's first do-catch disappears.

func get(for key: Key) -> Value? {
  guard contains(key: key) else { return nil }
  // fetch value here
}

Mask the Exception at the Lower Level

Not every error must reach the client. Handle what you can where it happens. Here a network failure re-queues the record instead of throwing, so callers never see CKError.networkUnavailable.

final class CloudService {
  private let container = CKContainer.default()
  private var pending: Set<CKRecord> = []

  func save(_ user: CKRecord) async throws {
    pending.insert(user)
    while let user = pending.popFirst() {
      do {
        try await container.privateCloudDatabase.save(user)
      } catch CKError.networkUnavailable {
        pending.insert(user)
        break
      }
    }
  }
}

Aggregate Errors the Caller Treats the Same

Errors that only get logged or shown in an alert do not each need a case. Fold them into one general case carrying the message, and keep dedicated cases only for failures the caller handles differently.

enum ErrorKind: Error {
  case general(String)
  case outOfMemory
}

func loadFromDisk() async throws {
  do {
    storage = try JSONDecoder().decode([Key: Value].self, from: data)
  } catch {
    throw ErrorKind.general(error.localizedDescription)
  }
}

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-swiftui-guide({ topic: "swiftui-modeling-errors-in-swift" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems