Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/superfly/sprites-go/llms.txt

Use this file to discover all available pages before exploring further.

The Sprites Go SDK surfaces errors through the same patterns as the standard library’s os/exec package, with additions for API-level failures like rate limiting and authentication errors. Understanding which error type you receive tells you whether a problem originated in the remote process, the network, or the API itself.

Process errors: ExitError

When a command exits with a non-zero status code, Run, Wait, and Output return an *ExitError:
type ExitError struct {
    Code int
}

func (e *ExitError) Error() string   // "exit status N"
func (e *ExitError) ExitCode() int   // returns Code
Use errors.As to unwrap it and read the exit code:
cmd := sprite.Command("false")
err := cmd.Run()

var exitErr *sprites.ExitError
if errors.As(err, &exitErr) {
    fmt.Printf("process exited with code %d\n", exitErr.ExitCode())
} else if err != nil {
    // Connection, auth, or other infrastructure error.
    log.Fatal(err)
}
The SDK also accepts the direct type assertion form shown in the README — err.(*sprites.ExitError) — but errors.As is preferred because it correctly unwraps errors that have been wrapped with fmt.Errorf("%w", ...).

Distinguishing process errors from infrastructure errors

A concrete decision tree for the errors returned by Run / Wait:
err := cmd.Run()
if err == nil {
    // Success, exit code 0.
    return
}

var exitErr *sprites.ExitError
switch {
case errors.As(err, &exitErr):
    // The process ran and exited with a non-zero code.
    fmt.Println("exit code:", exitErr.ExitCode())

case errors.Is(err, context.DeadlineExceeded):
    // The context timed out before the process finished.
    fmt.Println("command timed out")

case errors.Is(err, context.Canceled):
    // The context was cancelled explicitly.
    fmt.Println("command cancelled")

default:
    // Network, WebSocket, or authentication failure.
    fmt.Println("infrastructure error:", err)
}

ErrNotStarted

Wait returns ErrNotStarted when called before Start. This is the equivalent of calling Wait on an exec.Cmd that was never started:
var ErrNotStarted = errors.New("sprite: command not started")
cmd := sprite.Command("echo", "hello")
// Calling Wait before Start returns ErrNotStarted.
err := cmd.Wait()
if errors.Is(err, sprites.ErrNotStarted) {
    log.Fatal("you must call Start before Wait")
}
The correct sequence is always Start → (optional work) → Wait, or just Run which does both:
// Correct: Start then Wait.
if err := cmd.Start(); err != nil {
    log.Fatal(err)
}
// ... do other work ...
if err := cmd.Wait(); err != nil {
    log.Fatal(err)
}

“Already started” error

Calling Start a second time on the same Cmd returns an error immediately:
// From exec.go:
// if c.started {
//     return errors.New("sprite: already started")
// }
cmd := sprite.Command("sleep", "5")

if err := cmd.Start(); err != nil {
    log.Fatal(err)
}

// Calling Start again on the same Cmd returns an error.
if err := cmd.Start(); err != nil {
    fmt.Println(err) // "sprite: already started"
}
Create a new Cmd each time you want to run a command. Cmd values are not reusable.

API errors

HTTP errors from the Sprites API are returned as *APIError:
type APIError struct {
    ErrorCode         string // machine-readable code, e.g. "sprite_creation_rate_limited"
    Message           string // human-readable message
    Limit             int    // rate limit value
    WindowSeconds     int    // rate limit window
    RetryAfterSeconds int    // seconds to wait before retrying (from JSON body)
    CurrentCount      int    // current concurrent count
    UpgradeAvailable  bool
    UpgradeURL        string
    StatusCode        int    // HTTP status code
    RetryAfterHeader  int    // seconds from Retry-After header
}
Use sprites.IsAPIError to check for and extract API errors:
_, err := client.CreateSprite(ctx, "my-sprite", nil)
if apiErr := sprites.IsAPIError(err); apiErr != nil {
    fmt.Printf("API error %d: %s\n", apiErr.StatusCode, apiErr.Message)
}

Rate limit errors

The API uses two rate-limit error codes:
ConstantValueMeaning
ErrCodeCreationRateLimited"sprite_creation_rate_limited"Too many sprites created in the rate-limit window.
ErrCodeConcurrentLimitExceeded"concurrent_sprite_limit_exceeded"Too many sprites running at once.
Check for rate limits with the helper functions:
_, err := client.CreateSprite(ctx, "my-sprite", nil)

if apiErr := sprites.IsRateLimitErr(err); apiErr != nil {
    wait := apiErr.GetRetryAfterSeconds()
    fmt.Printf("rate limited — retry after %d seconds\n", wait)
    time.Sleep(time.Duration(wait) * time.Second)
    // retry...
}
Or inspect the code directly on the APIError:
if apiErr := sprites.IsAPIError(err); apiErr != nil {
    switch {
    case apiErr.IsCreationRateLimited():
        fmt.Printf("creation limit: %d per %ds, retry after %ds\n",
            apiErr.Limit, apiErr.WindowSeconds, apiErr.GetRetryAfterSeconds())

    case apiErr.IsConcurrentLimitExceeded():
        fmt.Printf("concurrent limit exceeded (%d running)\n", apiErr.CurrentCount)
        if apiErr.UpgradeAvailable {
            fmt.Println("upgrade at:", apiErr.UpgradeURL)
        }
    }
}
GetRetryAfterSeconds prefers the value from the JSON response body and falls back to the Retry-After HTTP header.

Context cancellation and timeouts

Pass a context to CommandContext to impose a deadline. When the context expires the command is killed and Run / Wait return a context error:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

cmd := sprite.CommandContext(ctx, "long-running-task")
err := cmd.Run()

if errors.Is(err, context.DeadlineExceeded) {
    fmt.Println("command exceeded the 30-second limit")
}
For explicit cancellation:
ctx, cancel := context.WithCancel(context.Background())

cmd := sprite.CommandContext(ctx, "watch", "-n1", "date")
go func() {
    time.Sleep(5 * time.Second)
    cancel() // Stop the command after 5 seconds.
}()

err := cmd.Run()
if errors.Is(err, context.Canceled) {
    fmt.Println("command was cancelled")
}
Contexts are also respected by checkpoint and list operations:
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

stream, err := sprite.CreateCheckpoint(ctx)
// If the checkpoint takes longer than 10 seconds, ctx.Err() is returned.

Sending signals

Use Signal to send a POSIX signal to the running process. Valid signal names are: INT, TERM, HUP, KILL, QUIT, USR1, USR2.
cmd := sprite.Command("sleep", "60")
if err := cmd.Start(); err != nil {
    log.Fatal(err)
}

// Interrupt the process.
if err := cmd.Signal("INT"); err != nil {
    log.Printf("signal error: %v", err)
}

// Wait returns an ExitError (or nil if the signal handler exited cleanly).
if err := cmd.Wait(); err != nil {
    var exitErr *sprites.ExitError
    if errors.As(err, &exitErr) {
        fmt.Println("process exited after signal, code:", exitErr.ExitCode())
    }
}
Signal returns an error if the process has not been started or has already finished:
// Before Start: "sprite: Signal before process started"
// After Wait:   "sprite: Signal after process finished"

Connection errors

When the WebSocket connection is dropped before the process exits, Wait returns "connection closed". This is distinct from an ExitError (process ran to completion with a non-zero code) and from a context error (explicit cancellation or deadline):
err := cmd.Wait()

var exitErr *sprites.ExitError
switch {
case err == nil:
    fmt.Println("success")
case errors.As(err, &exitErr):
    fmt.Println("non-zero exit:", exitErr.ExitCode())
case errors.Is(err, context.DeadlineExceeded), errors.Is(err, context.Canceled):
    fmt.Println("context error:", err)
default:
    // Includes "connection closed" and network-level errors.
    fmt.Println("connection or infrastructure error:", err)
}

Error handling best practices

Every command execution can fail due to a non-zero exit code, a lost connection, or a context cancellation. Ignoring the returned error masks real failures.
// Bad: ignoring the error.
cmd.Run()

// Good: handling the error.
if err := cmd.Run(); err != nil {
    var exitErr *sprites.ExitError
    if errors.As(err, &exitErr) {
        fmt.Println("exit code:", exitErr.ExitCode())
    } else {
        log.Fatal(err)
    }
}
The SDK wraps some errors with fmt.Errorf("%w", ...). errors.As traverses the chain correctly; a bare type assertion does not.
// Fragile: fails if the error is wrapped.
if exitErr, ok := err.(*sprites.ExitError); ok { ... }

// Robust: works regardless of wrapping.
var exitErr *sprites.ExitError
if errors.As(err, &exitErr) { ... }
Commands that talk to external services or perform heavy computation should always run under a context with a deadline. This prevents goroutine leaks when the sprite becomes unresponsive.
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()

cmd := sprite.CommandContext(ctx, "make", "build")
if err := cmd.Run(); err != nil {
    log.Fatal(err)
}
When the API returns a rate-limit error, wait the number of seconds indicated before retrying. Do not retry immediately.
for attempts := 0; attempts < 3; attempts++ {
    _, err := client.CreateSprite(ctx, "my-sprite", nil)
    if err == nil {
        break
    }
    if apiErr := sprites.IsRateLimitErr(err); apiErr != nil {
        wait := apiErr.GetRetryAfterSeconds()
        if wait == 0 {
            wait = 5 // sensible default
        }
        time.Sleep(time.Duration(wait) * time.Second)
        continue
    }
    log.Fatal(err) // non-retryable error
}
A Cmd cannot be reused. After Run or Wait returns, discard the value and create a new one with sprite.Command.
// Bad: reusing the same Cmd.
cmd := sprite.Command("echo", "hello")
cmd.Run()
cmd.Run() // returns "sprite: already started"

// Good: create a new Cmd each time.
for i := 0; i < 3; i++ {
    if err := sprite.Command("echo", "hello").Run(); err != nil {
        log.Fatal(err)
    }
}

Build docs developers (and LLMs) love