Back to Microsandbox

Error handling

docs/sdk/errors.mdx

0.6.912.7 KB
Original Source

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
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
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
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
// 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
from microsandbox import SandboxAlreadyExistsError

try:
    sb = await Sandbox.create("worker", image="alpine")
except SandboxAlreadyExistsError:
    print("sandbox already exists; resume or pass replace=True")
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 Naming conflicts 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
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
// The Go SDK surfaces boot failures as *m.Error.
// Inspect Kind and Message for the failure details.
_, 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

<Tooltip tip="Rust drop and TypeScript await using do not stop microsandbox cloud sandboxes, because cloud handles do not own the host process; call stop() or remove() explicitly."><span className="msb-badge-limited">Limited on cloud <Icon icon="circle-info" size={11} /></span></Tooltip>

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 Stop + 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
# 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
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.Stop(stopCtx)
    _ = sb.Close()
}()

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