Skip to main content
Chappie uses two typed error enums for failures you are expected to handle in production code. ChappieClientError covers everything that can go wrong during a request — missing credentials, usage caps, tool failures, transport errors, and unexpected server responses. ChappieAuthError covers failures during the sign-in flow itself. Both conform to LocalizedError, so error.localizedDescription always returns a human-readable string suitable for logging, even if you do not exhaustively switch on every case.

Catching ChappieClientError

All ChappieClient methods that communicate with the network are async throws. Wrap them in a do/catch block and match the cases you care about first, then fall through to a generic handler for anything unexpected.
do {
    let reply = try await client.send("Hello")
} catch ChappieClientError.missingCredential {
    // User needs to sign in
} catch ChappieClientError.usageLimitReached(let limit) {
    print(limit.plan?.displayName ?? "Unknown")
    print("Resets:", limit.resetsAt as Any)
} catch ChappieClientError.reauthenticationRequired(let message) {
    // Token expired beyond refresh — show sign-in again
} catch ChappieClientError.httpError(let status, let body) {
    print("HTTP \(status): \(body)")
} catch ChappieClientError.streamingTransportReplayDiverged(let partial) {
    // Streaming reconnect diverged — show error, partial text may be non-empty
} catch ChappieClientError.toolLoopLimitExceeded(let limit) {
    print("Exceeded \(limit) tool call rounds")
} catch {
    print(error.localizedDescription)
}

ChappieClientError Cases

Every case in ChappieClientError maps to a distinct failure mode. Expand a case to see what it means and how to respond.
The client attempted a request but found no stored credential in Keychain. The user has never signed in, or credentials were cleared.Response: Direct the user to sign in with ChappieSignIn or by calling authSession.startDeviceCodeSignIn().
A credential exists in Keychain but it contains no refresh token, so the SDK cannot renew an expired access token.Response: Ask the user to sign in again. Treat this the same as missingCredential in your UI.
Stored credentials do not include an account ID. This can happen if account metadata was partially written during an earlier interrupted sign-in.Response: Sign the user out and prompt them to sign in again to refresh the full credential set.
The access token has expired and the SDK could not refresh it — for example, because the refresh token has been revoked on the server. The associated String value is a human-readable reason.Response: Show the reason string to the user if it is informative, then present the sign-in flow again.
catch ChappieClientError.reauthenticationRequired(let message) {
    showSignInAlert(reason: message)
}
The ChatGPT usage cap for the user’s plan has been reached. The associated ChappieUsageLimit value carries plan, reset time, and window data — see ChappieUsageLimit fields below.Response: Show the user their plan name and when their quota resets. Avoid retrying immediately; the SDK’s built-in backoff already attempted retries before surfacing this error.
catch ChappieClientError.usageLimitReached(let limit) {
    let planName = limit.plan?.displayName ?? "your current plan"
    let resetTime = limit.resetsAt.map { $0.formatted(date: .abbreviated, time: .shortened) }
    showUsageBanner(plan: planName, resetsAt: resetTime)
}
The model called a tool by name, but no matching ChappieHostTool was registered on the client. The associated String is the tool name.Response: Register a handler for every tool declared in your ChappieHarness. If the tool is optional, register a handler that returns a graceful “unavailable” result rather than omitting the registration entirely.
A tool call was blocked — either by your app’s approval policy or by an explicit user denial. The first String is the tool name; the second is the denial reason.Response: Log the denial for debugging. In most cases this is expected behaviour when a user declines a permission prompt; no recovery action is needed.
A registered tool handler threw an error during execution. The first String is the tool name; the second is the error message from the handler.Response: Investigate the handler implementation. You can surface a user-facing error message using the tool name, but avoid exposing raw internal error strings to users.
The model kept calling tools beyond the SDK’s configured maximum number of tool-call rounds. The associated Int is the limit that was exceeded.Response: Show a generic “could not complete” message. If this happens regularly, review whether your tool definitions create loops, or increase the loop limit in client configuration.
catch ChappieClientError.toolLoopLimitExceeded(let limit) {
    print("Model exceeded \(limit) tool call rounds — possible tool loop")
}
The response stream ended with a terminal error status rather than a successful completion. The associated ChappieResponse contains the raw status and any error payload.Response: Log the response for debugging. Show a generic retry prompt to the user; the failure is likely transient.
After a WebSocket disconnect and reconnect, the replayed text did not match what was already delivered to your app. Chappie surfaces this error instead of emitting corrupted output. The associated String contains any partial text that was emitted before the divergence.Response: Show an error and give the user the option to retry. You can use the partial text string to reconstruct a best-effort display if needed.
catch ChappieClientError.streamingTransportReplayDiverged(let partial) {
    if partial.isEmpty {
        showRetryPrompt()
    } else {
        showPartialResultWithRetry(partial)
    }
}
The server returned an HTTP error response that the SDK could not map to a more specific error. The Int is the HTTP status code; the String is the response body.Response: Log the status and body. For 5xx errors, show a “service unavailable” message. For 4xx errors (other than 401 and 429, which the SDK handles automatically), check whether the request was well-formed.
catch ChappieClientError.httpError(let status, let body) {
    logger.error("HTTP \(status): \(body)")
    showGenericNetworkError()
}
The server returned a response the SDK could not parse. This is distinct from an HTTP error — the HTTP status was likely successful, but the body was malformed or had an unexpected shape.Response: Show a generic error. File a bug with Chappie support if this occurs consistently.

ChappieUsageLimit Fields

When you catch usageLimitReached, the associated ChappieUsageLimit value gives you structured data to build a meaningful error UI.
type
String
The error type string from the server. Typically "usage_limit_reached".
message
String?
A human-readable description of the limit that was hit. Safe to display directly to users when present.
plan
ChappiePlan?
The user’s current ChatGPT plan at the time the limit was reached. Use plan?.displayName for a user-facing plan name.
resetsAt
Date?
The date and time when the quota resets. Format this with Date.FormatStyle before showing it in your UI.
resetsInSeconds
TimeInterval?
Seconds until the quota resets, as reported by the server at response time. Useful for countdown timers; note that it is a point-in-time value, not a live countdown.
rateLimit
ChappieUsageLimitSnapshot?
Detailed window data with primary and secondary usage windows. Each window exposes usedPercent, windowMinutes, and resetsAt. Useful for building progress-bar or usage-meter UIs.

Auth Errors (ChappieAuthError)

ChappieAuthError is thrown by auth-session methods such as startDeviceCodeSignIn() and refreshAuth(). Handle it wherever you initiate or observe the sign-in flow.
The device code shown to the user was not approved before it expired.Response: Restart the sign-in flow. The expiry window is set by the authorization server, not the SDK.
The user cancelled the sign-in flow, or a CancellationError was thrown during signing in.Response: Return to the signed-out state silently. No error UI is needed unless the cancellation was unexpected.
A URLError occurred during an auth network call. The associated String is the localized description from URLError.Response: Show a network-error message and offer a retry.
The SDK polled the authorization server for device-code approval until the maximum polling duration was reached without a response.Response: Show a timeout message and offer the user a fresh sign-in attempt.
The SDK timed out waiting for the ChatGPT sign-in callback to arrive after the user approved the device code in their browser.Response: Show a timeout message and offer the user a fresh sign-in attempt.
The token exchange succeeded but the response did not include a refresh token.Response: Prompt the user to sign in again from scratch.
The OAuth token-exchange request returned an error HTTP status. statusCode is the HTTP status; body is the raw response body.Response: Log the status and body. For 400/401/403 responses, the requiresReauthentication property on the error is true — treat them as a sign-out and re-sign-in scenario.
The user explicitly denied the authorization request. The associated String is the denial message.Response: Respect the user’s decision. Show a message explaining that the app needs ChatGPT access to function, but do not re-prompt without a deliberate user action.
Secure random data generation failed at the OS level. The associated statusCode is the Int32 status returned by the system call.Response: Show a generic sign-in error. This is extremely rare; log the status code and file a bug if it recurs.
An auth response body was malformed or had an unexpected structure.Response: Show a generic sign-in error. File a bug if this occurs consistently.
An unexpected error occurred that the SDK could not map to a more specific case. The associated String is the localized description of the underlying error.Response: Log the message for debugging. Show a generic sign-in error to the user.

Rate Limiting and Automatic Backoff

Chappie automatically retries 429 responses before surfacing usageLimitReached or httpError. The default policy attempts 2 retries with an initial delay of 1 second, a maximum delay of 30 seconds, and honours the Retry-After header when the server provides one. To tighten or relax that behaviour, pass a custom ChappieRateLimitBackoffPolicy when creating the client:
let client = Chappie.client(
    rateLimitBackoff: ChappieRateLimitBackoffPolicy(
        maxRetries: 3,
        initialDelay: 1,
        maximumDelay: 15
    )
)
To disable automatic retries entirely and receive the error on the first 429, use the .disabled preset — which is equivalent to ChappieRateLimitBackoffPolicy(maxRetries: 0):
let noBackoffClient = Chappie.client(rateLimitBackoff: .disabled)
Disabling backoff is useful in test environments where you want deterministic error behaviour. Keep the default or a custom policy in production so transient rate-limit blips do not surface as errors to users.
ChappieRateLimitBackoffPolicy fields:
maxRetries
Int
default:"2"
Number of retry attempts before the SDK surfaces the error. Set to 0 for .disabled behaviour.
initialDelay
TimeInterval
default:"1"
Seconds to wait before the first retry. Subsequent retries use bounded exponential backoff up to maximumDelay.
maximumDelay
TimeInterval
default:"30"
Upper bound on the backoff interval in seconds, regardless of how many retries have been attempted.
respectsRetryAfterHeader
Bool
default:"true"
When true, the SDK uses the Retry-After value from the server response instead of computing its own delay. Set to false if you want purely client-side backoff timing.

Handling Stream Cancellation

When you cancel a streaming task — by calling handle.cancel() or cancelling a Swift Task — the stream throws either CancellationError or a URLError with code .cancelled. Both are normal and expected; catch them and take no action.
do {
    for try await event in handle.events {
        // Handle streaming events
    }
} catch is CancellationError {
    // Normal stream cancellation — no action needed
} catch let error as URLError where error.code == .cancelled {
    // Also normal — treat the same as CancellationError
} catch {
    // Unexpected error — log and surface to the user
    logger.error("Stream failed: \(error.localizedDescription)")
    showStreamError(error)
}
Do not surface CancellationError or URLError.cancelled to users. They indicate the stream was stopped deliberately by your code, not an unexpected failure.