Skip to content

Error Handling ​

Handle errors gracefully with categorized, retryable error types.

Overview ​

Conduit uses AIError as its primary error type. Every case is categorized, includes a recovery suggestion, and indicates whether automatic retry is appropriate. This gives you everything you need for robust error handling in production apps.

AIError Categories ​

Errors are organized into logical categories accessible via the category property:

Provider Errors ​

Issues with provider configuration or availability:

swift
do {
    let result = try await provider.generate("Hello", model: .claudeSonnet45)
} catch let error as AIError {
    switch error {
    case .providerUnavailable(let reason):
        // Provider is not reachable
        print("Provider unavailable: \(reason)")
    case .authenticationFailed(let message):
        // Invalid or missing API key
        print("Auth failed: \(message)")
    case .modelNotFound(let model):
        // Requested model doesn't exist
        print("Model not found: \(model.rawValue)")
    case .modelNotCached(let model):
        // Local model not downloaded yet
        print("Download \(model.displayName) first")
    case .incompatibleModel(let model, let reasons):
        // Model can't run on this device/config
        for reason in reasons { print(reason) }
    default:
        break
    }
}

Generation Errors ​

Issues during text generation:

  • .generationFailed(underlying:) — General generation failure
  • .tokenLimitExceeded(count:limit:) — Input exceeds context window
  • .contentFiltered(reason:) — Content was blocked by safety filters
  • .cancelled — Generation was cancelled
  • .timeout(TimeInterval) — Generation timed out

Network Errors ​

Connectivity and server issues:

  • .networkError(URLError) — Connection failed, DNS resolution, etc.
  • .serverError(statusCode:message:) — HTTP 5xx errors
  • .rateLimited(retryAfter:) — Rate limit hit with optional retry delay

Resource Errors ​

Local resource constraints:

  • .insufficientMemory(required:available:) — Not enough RAM for the model
  • .insufficientDiskSpace(required:available:) — Not enough storage
  • .downloadFailed(underlying:) — Model download failed
  • .checksumMismatch(expected:actual:) — Downloaded file is corrupted

Input Errors ​

Invalid input data:

  • .invalidInput(String) — Malformed input
  • .unsupportedAudioFormat(String) — Audio format not supported
  • .unsupportedLanguage(String) — Language not supported

The isRetryable Property ​

Check whether an error is safe to retry:

swift
do {
    let result = try await provider.generate("Hello", model: .claudeSonnet45)
} catch let error as AIError where error.isRetryable {
    // Safe to retry: rate limits, transient network errors, server errors
    try await Task.sleep(for: .seconds(2))
    let result = try await provider.generate("Hello", model: .claudeSonnet45)
} catch let error as AIError {
    // Not retryable: auth failures, invalid input, cancelled
    print("Permanent error: \(error.localizedDescription)")
}

Retryable errors include:

  • .rateLimited — Always retryable, respects retryAfter
  • .serverError — HTTP 5xx (transient)
  • .networkError — Some URLError codes (timeout, connection lost)
  • .timeout — Generation timeout

Non-retryable errors include:

  • .authenticationFailed — Fix your API key
  • .cancelled — User-initiated
  • .contentFiltered — Content policy violation
  • .invalidInput — Fix the input
  • .insufficientMemory — Device limitation

Recovery Suggestions ​

Every AIError provides a recoverySuggestion string suitable for user-facing display:

swift
catch let error as AIError {
    if let suggestion = error.recoverySuggestion {
        showAlert(message: suggestion)
        // e.g. "Check your API key and try again"
        // e.g. "Wait 30 seconds before retrying"
        // e.g. "Download the model before using it"
    }
}

Error Category Grouping ​

Use the category property to handle errors by group:

swift
catch let error as AIError {
    switch error.category {
    case .provider:
        // Configuration or availability issues
        showProviderSettings()
    case .generation:
        // Generation-specific failures
        retryOrShowError(error)
    case .network:
        // Connectivity problems
        showOfflineUI()
    case .resource:
        // Device resource constraints
        suggestSmallerModel()
    case .input:
        // Bad user input
        showInputValidationError(error)
    case .tool:
        // Tool execution failures
        showToolError(error)
    }
}

Retry with Exponential Backoff ​

A pattern for production retry logic:

swift
func generateWithRetry<P: TextGenerator>(
    provider: P,
    prompt: String,
    model: P.ModelID,
    maxAttempts: Int = 3
) async throws -> String {
    var lastError: Error?
    for attempt in 0..<maxAttempts {
        do {
            return try await provider.generate(prompt, model: model)
        } catch let error as AIError where error.isRetryable {
            lastError = error
            if case .rateLimited(let retryAfter) = error, let delay = retryAfter {
                try await Task.sleep(for: .seconds(delay))
            } else {
                let backoff = pow(2.0, Double(attempt))
                try await Task.sleep(for: .seconds(backoff))
            }
        }
    }
    throw lastError!
}

Released under the MIT License.