Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Error Handling

Gateway status failures and SDK validation failures use *v1.StatusError, which carries a machine-readable Code and a human-readable Message. Local I/O, transport setup, and sentinel errors may use other Go error types. The SDK provides predicate functions for classified status errors.

StatusError

When an operation returns a classified status error, inspect it directly or use the convenience predicates below. Use errors.Is for documented sentinel errors such as ErrTransportNotAvailable.

var se *v1.StatusError
if errors.As(err, &se) {
    fmt.Printf("code: %s, message: %s\n", se.Code, se.Message)
    // se.ErrorInfo and se.FieldViolations contain optional structured details.
}
FieldTypeDescription
CodeErrorCodeMachine-readable error classification
MessagestringHuman-readable error description
GRPCCodeint32Original gRPC code before SDK classification
FieldViolations[]FieldViolationRejected request fields and descriptions
ErrorInfo*ErrorInfoServer reason, domain, and metadata
RetryDelay*time.DurationSuggested minimum delay for an otherwise safe retry
CauseerrorOriginal gRPC error, including unknown details

Use status.FromError(err) to inspect the complete gRPC status through the SDK’s error wrapper. A delay does not prove that a timed-out mutation was not applied.

Predicate Functions

Use these top-level functions to check error types. They work with wrapped errors via errors.As.

FunctionErrorCodeWhen it fires
v1.IsNotFoundErrorNotFoundResource does not exist (sandbox, provider, etc.)
v1.IsAlreadyExistsErrorAlreadyExistsResource with that name already exists
v1.IsConflictErrorConflictOptimistic concurrency violation or invalid state transition
v1.IsUnavailableErrorUnavailableGateway is unreachable or client is closed
v1.IsUnimplementedErrorUnimplementedOperation not supported by the gateway version
v1.IsPermissionDeniedErrorPermissionDeniedInsufficient permissions
v1.IsInvalidArgumentErrorInvalidArgumentInvalid request parameters
v1.IsDeadlineExceededErrorDeadlineExceededOperation timed out
v1.IsCancelledErrorCancelledOperation was cancelled (context cancellation)

For ErrorInternal (server-side errors), no convenience predicate exists. Match it directly via the Code field:

var se *v1.StatusError
if errors.As(err, &se) && se.Code == v1.ErrorInternal {
    fmt.Println("Internal server error:", se.Message)
}

Common Patterns

Not Found

Handle missing resources gracefully:

sb, err := client.Sandboxes().Get(ctx, "default", "my-sandbox")
if v1.IsNotFound(err) {
    fmt.Println("Sandbox does not exist, creating...")
    sb, err = client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{})
}
if err != nil {
    log.Fatal(err)
}

Already Exists

Guard against duplicate creation:

_, err := client.Providers().Create(ctx, "default", &v1.Provider{
    Name: "openai",
    Type: "openai",
})
if v1.IsAlreadyExists(err) {
    fmt.Println("Provider already registered, skipping")
} else if err != nil {
    log.Fatal(err)
}

Alternatively, use Ensure for idempotent registration:

// Create-or-update in a single call
provider, err := client.Providers().Ensure(ctx, "default", &v1.Provider{
    Name: "openai",
    Type: "openai",
})

Conflict (Optimistic Concurrency)

When two clients modify the same resource concurrently, the second write receives a conflict error. Retry by re-reading the resource:

sb, _ := client.Sandboxes().Get(ctx, "default", "my-sandbox")

result, err := client.Sandboxes().AttachProvider(ctx,
    "default", "my-sandbox", "openai", sb.ResourceVersion,
)
if v1.IsConflict(err) {
    // Another client modified the sandbox — re-read and retry
    sb, _ = client.Sandboxes().Get(ctx, "default", "my-sandbox")
    result, err = client.Sandboxes().AttachProvider(ctx,
        "default", "my-sandbox", "openai", sb.ResourceVersion,
    )
}
if err != nil {
    log.Fatal(err)
}

Unavailable

Handle gateway connectivity issues:

sb, err := client.Sandboxes().Get(ctx, "default", "my-sandbox")
if v1.IsUnavailable(err) {
    fmt.Println("Gateway is not reachable, check connection")
    // Implement retry with backoff
}

Unimplemented

Detect unsupported operations gracefully:

_, err := client.Sandboxes().GetLogs(ctx, "default", "my-sandbox")
if v1.IsUnimplemented(err) {
    fmt.Println("Log retrieval not supported by this gateway version")
}

Retry with Backoff

For transient errors like Unavailable or DeadlineExceeded, use exponential backoff only when the operation is idempotent or carries an idempotency/concurrency token. A timeout does not prove that a mutating request was not applied.

func withRetry(ctx context.Context, maxAttempts int, fn func() error) error {
    backoff := 100 * time.Millisecond

    for attempt := 0; attempt < maxAttempts; attempt++ {
        err := fn()
        if err == nil {
            return nil
        }

        // Only retry transient errors
        if !v1.IsUnavailable(err) && !v1.IsDeadlineExceeded(err) {
            return err
        }

        // Add jitter to prevent thundering herd after outages
        jitter := time.Duration(rand.Int63n(int64(backoff) / 2))

        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-time.After(backoff + jitter):
            backoff *= 2
        }
    }
    return fmt.Errorf("exhausted %d retry attempts", maxAttempts)
}

Usage:

var sb *v1.Sandbox
err := withRetry(ctx, 3, func() error {
    var e error
    sb, e = client.Sandboxes().Get(ctx, "default", "my-sandbox")
    return e
})

Context Cancellation

All SDK methods accept a context.Context. Use context deadlines and cancellation to control timeouts:

// 5-second timeout for a single call
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

sb, err := client.Sandboxes().Get(ctx, "default", "my-sandbox")
if v1.IsDeadlineExceeded(err) {
    fmt.Println("Request timed out")
}

See also: Testing for how the fake client returns the same error codes.