---
title: Error handling
description: Typed errors and resource cleanup patterns
icon: "triangle-exclamation"
---

All SDKs surface typed errors so you can match on specific failure modes instead of parsing strings. Rust has an `Error` enum, TypeScript exposes a dedicated subclass per variant (use `instanceof`), Python provides dedicated exception classes, and Go provides an `*Error` value with an `ErrorKind` discriminator matched via `m.IsKind(err, kind)` or `errors.As`.

## Matching errors

<CodeGroup>
```rust Rust
use microsandbox::{Sandbox, Error};

async fn get_or_create(name: &str) -> Result<Sandbox, Error> {
    match Sandbox::get(name).await {
        Ok(handle) => handle.start().await,
        Err(Error::SandboxNotFound(_)) => {
            Sandbox::builder(name).image("python").create().await
        }
        Err(e) => Err(e),
    }
}

match sb.exec("python", ["script.py"]).await {
    Ok(output) if output.status().success => {
        println!("{}", output.stdout()?);
    }
    Ok(output) => {
        eprintln!("Exit {}: {}", output.status().code, output.stderr()?);
    }
    Err(Error::ExecTimeout) => eprintln!("Timed out"),
    Err(Error::Runtime(msg)) => eprintln!("Runtime: {msg}"),
    Err(e) => return Err(e),
}
```

```typescript TypeScript
import {
    ExecTimeoutError,
    RuntimeError,
    Sandbox,
    SandboxNotFoundError,
} from "microsandbox";

async function getOrCreate(name: string): Promise<Sandbox> {
    try {
        const handle = await Sandbox.get(name);
        return await handle.start();
    } catch (e) {
        if (e instanceof SandboxNotFoundError) {
            return Sandbox.builder(name).image("python").create();
        }
        throw e;
    }
}

try {
    const output = await sb.exec("python", ["script.py"]);
    if (!output.success) {
        console.error(`Failed (exit ${output.code}):`, output.stderr());
    }
} catch (e) {
    if (e instanceof ExecTimeoutError) {
        console.error(`Timed out after ${e.timeoutMs}ms`);
    } else if (e instanceof RuntimeError) {
        console.error("Runtime:", e.message);
    } else {
        throw e;
    }
}
```

```python Python
from microsandbox import (
    ExecTimeoutError, Sandbox, SandboxNotFoundError
)

async def get_or_create(name: str):
    try:
        handle = await Sandbox.get(name)
        return await handle.start()
    except SandboxNotFoundError:
        return await Sandbox.create(name, image="python")

try:
    output = await sb.exec("python", ["script.py"])
    if not output.success:
        print(f"Exit {output.exit_code}: {output.stderr_text}")
except ExecTimeoutError:
    print("Timed out")
```

```go Go
import (
    "context"
    "errors"
    "log"

    m "github.com/superradcompany/microsandbox/sdk/go"
)

func getOrCreate(ctx context.Context, name string) (*m.Sandbox, error) {
    handle, err := m.GetSandbox(ctx, name)
    if err != nil {
        if m.IsKind(err, m.ErrSandboxNotFound) {
            return m.CreateSandbox(ctx, name,
                m.WithImage("python"))
        }
        return nil, err
    }
    return handle.Start(ctx)
}

out, err := sb.Exec(ctx, "python", []string{"script.py"})
switch {
case err == nil && !out.Success():
    log.Printf("exit %d: %s", out.ExitCode(), out.Stderr())
case m.IsKind(err, m.ErrExecTimeout):
    log.Println("timed out")
case err != nil:
    // errors.As for deeper inspection.
    var me *m.Error
    if errors.As(err, &me) {
        log.Printf("kind=%s message=%s", me.Kind, me.Message)
    }
}
```

</CodeGroup>

## Spawn-time exec failures

`exec()` distinguishes between:

- **A program that ran and exited non-zero** — the call returns an `ExecOutput` with a non-zero `code`. This is *not* an error in the SDK sense; it's a normal result.
- **A program that never started** — the binary doesn't exist, isn't executable, the working directory is unreachable, etc. This surfaces as a typed error variant: `ExecFailed` (Rust), `ExecFailedError` (TypeScript), `ExecFailedError` (Python).

The typed error carries a classified `kind` plus the underlying `errno`, so callers can branch on the cause and react. Common kinds: `NotFound` (binary missing on PATH), `PermissionDenied`, `NotExecutable`, `BadCwd`, `BadArgs`, `ResourceLimit`, `UserSetupFailed`, `OutOfMemory`, `PtySetupFailed`, `Other`.

<CodeGroup>
```rust Rust
use microsandbox::Error;
use microsandbox_protocol::exec::ExecFailureKind;

match sb.exec("nonexistent", []).await {
    Ok(output) => { /* program ran, check output.status() */ }
    Err(Error::ExecFailed(payload)) => {
        match payload.kind {
            ExecFailureKind::NotFound => {
                eprintln!("Binary not found on PATH: {}", payload.message);
            }
            ExecFailureKind::PermissionDenied => {
                eprintln!("Not executable (chmod +x?): {}", payload.message);
            }
            kind => {
                eprintln!("Spawn failed ({:?}): {}", kind, payload.message);
            }
        }
        // payload.errno, payload.errno_name, payload.stage are also available
    }
    Err(e) => return Err(e),
}
```

```typescript TypeScript
import { ExecFailedError, Sandbox } from "microsandbox";

try {
    const output = await sb.exec("nonexistent");
    // program ran, check output.success / output.code
} catch (e) {
    if (e instanceof ExecFailedError) {
        switch (e.kind) {
            case "not_found":
                console.error("Binary not on PATH:", e.message);
                break;
            case "permission_denied":
                console.error("Not executable (chmod +x?):", e.message);
                break;
            default:
                console.error(`Spawn failed (${e.kind}):`, e.message);
        }
        // e.errno, e.errnoName, e.stage are also available
    } else {
        throw e;
    }
}
```

```python Python
from microsandbox import ExecFailedError

try:
    output = await sb.exec("nonexistent")
    # program ran, check output.success / output.exit_code
except ExecFailedError as e:
    if e.kind == "not_found":
        print(f"Binary not on PATH: {e.message}")
    elif e.kind == "permission_denied":
        print(f"Not executable (chmod +x?): {e.message}")
    else:
        print(f"Spawn failed ({e.kind}): {e.message}")
    # e.errno, e.errno_name, e.stage are also available
```

```go Go
// Streaming exec surfaces spawn-failure detail via ExecEventFailed.
h, err := sb.ExecStream(ctx, "nonexistent", nil)
if err != nil {
    return err
}
defer h.Close()

for {
    ev, err := h.Recv(ctx)
    if err != nil {
        return err
    }
    switch ev.Kind {
    case m.ExecEventExited:
        // Program ran — inspect ev.ExitCode.
    case m.ExecEventFailed:
        f := ev.Failure // *m.ExecFailure
        switch f.Kind {
        case "not_found":
            log.Printf("Binary not on PATH: %s", f.Message)
        case "permission_denied":
            log.Printf("Not executable (chmod +x?): %s", f.Message)
        default:
            log.Printf("Spawn failed (%s): %s", f.Kind, f.Message)
        }
        // f.Errno (*int), f.ErrnoName, f.Path are also available.
    case m.ExecEventDone:
        return nil
    }
}
```
</CodeGroup>

The CLI maps these kinds to POSIX-style exit codes: `127` for `NotFound`, `126` for `PermissionDenied` and `NotExecutable`, `1` otherwise. SDK callers reading the error directly don't need to think about exit codes — branch on `kind` instead.

## Name conflicts

Creating a sandbox with a name that's already in use (and without `replace`) surfaces a typed error you can branch on to decide whether to recover (resume the existing one, regenerate the name, etc.).

<CodeGroup>
```rust Rust
use microsandbox::Error;

match Sandbox::builder("worker").image("alpine").create().await {
    Ok(sb) => { /* ... */ }
    Err(Error::SandboxAlreadyExists(name)) => {
        eprintln!("sandbox {name} already exists; resume or pass .replace()");
    }
    Err(e) => return Err(e),
}
```

```typescript TypeScript
import { SandboxAlreadyExistsError } from "microsandbox";

try {
    const sb = await Sandbox.builder("worker").image("alpine").create();
} catch (e) {
    if (e instanceof SandboxAlreadyExistsError) {
        console.error("sandbox already exists; resume or pass replace()");
    } else {
        throw e;
    }
}
```

```python Python
from microsandbox import SandboxAlreadyExistsError

try:
    sb = await Sandbox.create("worker", image="alpine")
except SandboxAlreadyExistsError:
    print("sandbox already exists; resume or pass replace=True")
```

```go Go
sb, err := m.CreateSandbox(ctx, "worker",
    m.WithImage("alpine"))
if m.IsKind(err, m.ErrSandboxAlreadyExists) {
    log.Println("sandbox already exists; resume or pass WithReplace()")
}
```

</CodeGroup>

Pass `replace()` / `replace=True` / `--replace` / `WithReplace()` to stop the existing sandbox and recreate it. See [Replace existing](/sandboxes/overview#replace-existing) for the grace-period knob.

## Sandbox start failures

When a sandbox process exits before the agent relay is ready (mount errors, missing rootfs, network setup failures), the SDK surfaces a typed `BootStart` / `BootStartError`. The payload carries the failure stage and errno so callers can recover or report cleanly.

<CodeGroup>
```rust Rust
use microsandbox::Error;
use microsandbox_runtime::boot_error::BootErrorStage;

match Sandbox::builder("svc").image("alpine").create().await {
    Ok(sb) => { /* ... */ }
    Err(Error::BootStart { name, err }) => {
        eprintln!("Sandbox {name:?} failed at stage {:?}: {}", err.stage, err.message);
        if matches!(err.stage, BootErrorStage::Mount) {
            eprintln!("Hint: a host volume path may not exist.");
        }
    }
    Err(e) => return Err(e),
}
```

```typescript TypeScript
import { BootStartError } from "microsandbox";

try {
    const sb = await Sandbox.builder("svc").image("alpine").create();
} catch (e) {
    if (e instanceof BootStartError) {
        console.error(`Sandbox "${e.name}" failed at stage ${e.stage}: ${e.message}`);
        if (e.stage === "mount") {
            console.error("Hint: a host volume path may not exist.");
        }
    } else {
        throw e;
    }
}
```

```python Python
from microsandbox import BootStartError

try:
    sb = await Sandbox.create("svc", image="alpine")
except BootStartError as e:
    print(f"Sandbox {e.name!r} failed at stage {e.stage}: {e.message}")
    if e.stage == "mount":
        print("Hint: a host volume path may not exist.")
```

```go Go
// The Go SDK currently surfaces boot failures as a generic *m.Error
// (typically Kind=ErrInternal or ErrInvalidConfig). A dedicated BootStart
// kind with stage+errno detail lands in a future release.
_, err := m.CreateSandbox(ctx, "svc", m.WithImage("alpine"))
var me *m.Error
if errors.As(err, &me) {
    log.Printf("sandbox boot failed (kind=%s): %s", me.Kind, me.Message)
}
```
</CodeGroup>

The CLI prepends the same payload as a styled `error:` block before any captured log output, so you see "what went wrong + a hint" inline. SDK callers get the structured payload to make their own decisions.

## Resource cleanup

Sandboxes hold compute resources, so release them when done. In Rust, `Drop` handles cleanup when the sandbox goes out of scope. In TypeScript, prefer `await using` (Node 22+) which calls `Sandbox.stop()` automatically when the binding leaves scope. In Go, pair every `CreateSandbox` with a `defer` that calls `StopAndWait` + `Close`.

<CodeGroup>
```rust Rust
use microsandbox::Sandbox;

// Sandbox implements Drop, so resources are released when `sb` goes out of scope.
// For explicit control, call stop() or kill().
{
    let sb = Sandbox::builder("temp")
        .image("python")
        .create()
        .await?;

    let output = sb.exec("python", ["-c", "print('hello')"]).await?;
} // sb is dropped here, resources are cleaned up
```

```typescript TypeScript
async function runTemporary(): Promise<string> {
    // `await using` calls Sandbox.stop() when the binding leaves scope.
    await using sb = await Sandbox.builder("temp")
        .image("python")
        .replace()
        .create();

    const out = await sb.exec("python", ["-c", "print('hello')"]);
    return out.stdout();
}
```

```python Python
# Use async context manager — auto-kills and removes on exit.
async with await Sandbox.create("temp", image="python") as sb:
    output = await sb.exec("python", ["-c", "print('hello')"])
    print(output.stdout_text)
```

```go Go
sb, err := m.CreateSandbox(ctx, "temp",
    m.WithImage("python"),
    m.WithReplace(),
)
if err != nil {
    log.Fatal(err)
}
defer func() {
    stopCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    _, _ = sb.StopAndWait(stopCtx)
    _ = sb.Close()
}()

out, _ := sb.Exec(ctx, "python", []string{"-c", "print('hello')"})
fmt.Println(out.Stdout())
```

</CodeGroup>
