Error Handling Patterns and Application Error Codes in apple/container: A Complete Guide

The apple/container repository implements a layered Swift error handling strategy that combines domain-specific enum errors, a protocol-based AppError system with stable string codes, and POSIX error bridging for system-level operations.

Understanding how the apple/container project manages failures is essential for contributing to or extending this containerization framework. The codebase adopts idiomatic Swift error handling patterns while providing structured application error codes that enable programmatic error classification. This guide examines the implementation details, key source files, and practical usage patterns found throughout the repository.

Swift Enum-Based Error Patterns

The foundation of error handling in apple/container relies on local Swift enum errors that conform to Swift.Error and CustomStringConvertible. Each module defines its own error enum to capture validation failures and unexpected states specific to that domain.

Local Error Definitions in ProgressConfig.swift

In ProgressConfig.swift, errors are defined as a simple enum with associated values for detailed failure reasons:

public enum Error: Swift.Error, CustomStringConvertible {
    case invalid(String)

    public var description: String {
        switch self {
        case .invalid(let reason):
            return "failed to validate config (\(reason))"
        }
    }
}

This pattern provides human-readable descriptions through the description property, which the codebase uses for logging and user-visible messages without exposing raw error objects.

Builder.swift Error Types

The Builder.swift file defines a more comprehensive error enum that handles various build-time failures:

public enum Error: Swift.Error, CustomStringConvertible {
    case invalidContinuation
    case buildComplete
    case invalidExport(String, String)
}

These cases capture specific failure modes such as invoking operations after build completion or validating export entries. The invalidExport case includes two associated strings to identify the problematic entry and the specific validation failure reason.

Plugin and Build Service Errors

Other modules follow the same structural pattern. PluginsService.swift defines errors like pluginNotFound and conflict, while BuildStdio.swift and BuildRemoteContentProxy.swift expose domain-specific cases that map directly to their respective failure points. This modular approach keeps error definitions close to their usage contexts while maintaining consistency across the codebase.

Structured Application Errors with AppError Protocol

For errors requiring programmatic classification, apple/container implements a structured error protocol that couples exceptions with stable, machine-readable codes.

The AppError Protocol Definition

Defined in ApplicationError.swift, the AppError protocol establishes a contract for errors that need metadata and stable identifiers:

public protocol AppError: Error {
    var code: AppErrorCode { get }
    var metadata: OrderedDictionary<String, String> { get }
    var underlyingError: Error? { get }
}

Concrete implementations like ResourceLabels.LabelError provide these properties, allowing callers to inspect code to determine appropriate recovery actions—such as rejecting invalid user input versus handling system bugs differently.

AppErrorCode Implementation

The AppErrorCode struct provides lightweight, string-based error classification:

public struct AppErrorCode: RawRepresentable, Hashable, Sendable {
    public let rawValue: String
    public init(rawValue: String) { self.rawValue = rawValue }
}

Common codes defined in ApplicationError.swift and extended in ResourceLabels.swift include:

  • invalid_argument for generic caller errors such as bad CLI flags
  • invalid_label_key_content for label keys containing illegal characters
  • invalid_label_key_length for keys exceeding 128 bytes
  • invalid_label_length for entire key=value pairs exceeding 4096 bytes

The extension in ResourceLabels.swift adds these label-specific codes as static properties:

extension AppErrorCode {
    public static let invalidLabelKeyContent = AppErrorCode(rawValue: "invalid_label_key_content")
    public static let invalidLabelKeyLength = AppErrorCode(rawValue: "invalid_label_key_length")
    public static let invalidLabelLength = AppErrorCode(rawValue: "invalid_label_length")
}

Resource Label Validation Example

When validating resource labels, the codebase uses the AppError protocol to provide detailed failure context:

do {
    let labels = try ResourceLabels(["com.example.key": "value"])
    // labels are safe to use
} catch let err as ResourceLabels.LabelError {
    print("Label error: \(err.code.rawValue)\(err.metadata)")
}

This pattern enables precise error handling based on specific validation failures while maintaining type safety.

POSIX Error Bridging for System Calls

Low-level system operations in apple/container require POSIX error bridging to convert C library failures into Swift errors.

Converting errno to Swift Errors

In Builder.swift, socket configuration functions demonstrate the bridging pattern:

private func setSockOpt(level: Int32, name: Int32, value: Int) throws {
    // ... socket setup code ...
    if res == -1 {
        throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EPERM)
    }
}

Higher-level callers catch POSIXError and often convert these system errors into domain-specific Swift errors. This creates a tidy boundary between system calls and the Swift-level error handling model, ensuring that components like FileHandle extensions propagate meaningful errors without leaking implementation details.

Error Propagation and Consumption Patterns

Functions that can fail are explicitly marked with throws, following idiomatic Swift conventions. The build method in Builder.swift demonstrates this pattern:

public func build(_ config: BuildConfig) async throws {
    guard let continuation else {
        throw Error.invalidContinuation
    }
    // ... additional validation ...
    try await self.client.performBuild(...)
}

Consumers use try await within Task or async contexts, allowing errors to bubble up to CLI entry points where they convert into user-visible messages or exit codes. This propagation model ensures that validation errors from ProgressConfig.swift, label errors from ResourceLabels.swift, and build errors from Builder.swift all surface consistently at the application boundary.

Summary

The apple/container repository implements a consistent, multi-layered error handling architecture:

  • Domain-specific enum errors provide type-safe failure representation with human-readable descriptions via CustomStringConvertible
  • The AppError protocol enables structured error classification using stable AppErrorCode strings for programmatic error handling
  • POSIX error bridging cleanly wraps system-level failures into the Swift error model
  • Explicit throws declarations maintain clear error propagation paths from low-level operations through to CLI interfaces

Frequently Asked Questions

What is the difference between local enum errors and AppError in apple/container?

Local enum errors defined in files like ProgressConfig.swift and Builder.swift provide module-specific validation failures with human-readable descriptions. The AppError protocol, defined in ApplicationError.swift, adds machine-readable error codes and structured metadata for errors that require programmatic handling, such as label validation failures in ResourceLabels.swift.

How does apple/container handle system-level POSIX errors?

The codebase converts POSIX errors to Swift errors using POSIXError and POSIXErrorCode. In Builder.swift, low-level socket operations check errno and throw POSIXError when system calls return -1. Higher-level components typically catch these and convert them to domain-specific errors, maintaining separation between system interfaces and application logic.

Where are application error codes defined in the repository?

Application error codes are defined in ApplicationError.swift as static properties on the AppErrorCode struct. Common codes like invalid_argument reside there, while module-specific extensions like those in ResourceLabels.swift add specialized codes such as invalid_label_key_content and invalid_label_length.

How should I handle errors when calling the build API in apple/container?

When calling builder.build(_ config: BuildConfig), use try await within a do-catch block. Catch specific Builder.Error cases like invalidExport to handle validation failures, or catch the general AppError protocol to inspect error codes programmatically. All build-related errors propagate through the async throws interface, allowing you to handle them at the appropriate abstraction level.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →