Back to Microsandbox

Error handling

docs/sdk/errors.mdx

0.6.1712.9 KB
Original Source

TypeScript, Rust, Python, and Go surface typed errors so you can match specific failure modes instead of parsing strings. TypeScript exposes a dedicated subclass per variant (use instanceof), Rust has an Error enum, Python provides dedicated exception classes, and Go provides an *Error value with an ErrorKind discriminator matched via m.IsKind(err, kind) or errors.As. Ruby currently exposes Microsandbox::Error; where no dedicated subclass exists, its reference documents the stable message contract explicitly.

Matching errors

<CodeGroup> ```typescript TypeScript import { ExecTimeoutError, RuntimeError, Sandbox, } from "microsandbox";

const sb = await Sandbox.builder("worker") .image("python") .connectOrCreate();

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; } }


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

let sb = Sandbox::builder("worker")
    .image("python")
    .connect_or_create()
    .await?;

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),
}
python
from microsandbox import ExecTimeoutError, Sandbox

sb = await Sandbox.connect_or_create("worker", 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"
)

sb, err := m.ConnectOrCreateSandbox(ctx, "worker", m.WithImage("python"))
if err != nil {
    log.Fatal(err)
}

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)
    }
}
ruby
require "microsandbox"

sb = Microsandbox::Sandbox.connect_or_create("worker", image: "python")

begin
  output = sb.exec("python", ["script.py"])
  warn "Exit #{output.exit_code}: #{output.stderr}" unless output.success?
rescue Microsandbox::Error => error
  warn error.message
end
</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. The call returns or raises an SDK error.

Rust exposes a structured ExecFailed payload, and Go exposes the same detail on streaming execution events. Common failure kinds include NotFound (binary missing on PATH), PermissionDenied, NotExecutable, BadCwd, BadArgs, ResourceLimit, UserSetupFailed, OutOfMemory, PtySetupFailed, and Other.

<CodeGroup> ```typescript TypeScript try { const output = await sb.exec("nonexistent"); // program ran, check output.success / output.code } catch (e) { console.error("Program could not be started:", e); } ```
rust
use microsandbox::{protocol::exec::ExecFailureKind, Error};

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),
}
python
from microsandbox import MicrosandboxError

try:
    output = await sb.exec("nonexistent")
    # program ran, check output.success / output.exit_code
except MicrosandboxError as e:
    print(f"Program could not be started: {e}")
ruby
require "microsandbox"

begin
  output = sandbox.exec("nonexistent")
  # program ran, check output.success / output.exit_code
rescue Microsandbox::Error => e
  warn "Program could not be started: #{e.message}"
end
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, and 1 otherwise.

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> ```typescript TypeScript import { Sandbox, 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; } }


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

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),
}
python
from microsandbox import Sandbox, 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()")
}
ruby
begin
  sb = Microsandbox::Sandbox.create("worker", image: "alpine")
rescue Microsandbox::Error => error
  raise unless error.message.include?("already exists")
  warn "sandbox already exists; use connect_or_create to reuse it or replace: true to recreate it"
end
</CodeGroup>

Use connect_or_create and its language-specific equivalents to reuse the existing sandbox without changing its configuration. Pass replace() / replace=True / replace: true / --replace / WithReplace() only when you intend to stop the existing sandbox and create a new one. See Naming conflicts for the grace-period setting.

When a sandbox object is stale

A Sandbox or SandboxHandle keeps the ID of the exact sandbox it represents. If that sandbox is removed and the name is reused, lifecycle methods refuse to act on the replacement and return a typed error.

<CodeGroup> ```typescript TypeScript import { SandboxReplacedError } from "microsandbox";

try { await staleHandle.destroy(); } catch (error) { if (!(error instanceof SandboxReplacedError)) throw error; }


```rust Rust
match stale_handle.destroy().await {
    Err(Error::SandboxReplaced { expected, actual, .. }) => {
        eprintln!("refusing stale operation: {expected} -> {actual}");
    }
    result => result?,
}
python
from microsandbox import SandboxReplacedError

try:
    await stale_handle.destroy()
except SandboxReplacedError:
    pass
go
if err := staleHandle.Destroy(ctx); m.IsKind(err, m.ErrSandboxReplaced) {
    log.Println("refusing stale lifecycle operation")
}
ruby
begin
  stale_handle.destroy
rescue Microsandbox::Error => error
  raise unless error.message.include?("was replaced")
end
</CodeGroup>

Ruby does not yet expose a dedicated stale-identity subclass. Until it does, Microsandbox::Error with the stable was replaced message is the Ruby-specific contract; the operation still refuses to act on the replacement.

Sandbox start failures

When a sandbox process exits before the agent relay is ready, creation returns or raises an SDK error. Rust exposes a structured BootStart payload with the failure stage and underlying message.

<CodeGroup> ```typescript TypeScript import { Sandbox } from "microsandbox";

try { const sb = await Sandbox.builder("svc").image("alpine").create(); } catch (e) { console.error("Sandbox failed to start:", e); }


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

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);
    }
    Err(e) => return Err(e),
}
python
from microsandbox import MicrosandboxError, Sandbox

try:
    sb = await Sandbox.create("svc", image="alpine")
except MicrosandboxError as e:
    print(f"Sandbox failed to start: {e}")
go
_, err := m.CreateSandbox(ctx, "svc", m.WithImage("alpine"))
if err != nil {
    log.Printf("sandbox failed to start: %v", err)
}
ruby
require "microsandbox"

begin
  sandbox = Microsandbox::Sandbox.create("svc", image: "alpine")
rescue Microsandbox::Error => e
  warn "Sandbox failed to start: #{e.message}"
end
</CodeGroup>

The CLI prints the startup failure before any captured log output so the immediate cause stays visible.

Resource cleanup

<Tooltip tip="TypeScript await using and Rust drop 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 TypeScript, prefer await using (Node 22+) which calls Sandbox.stop() automatically when the binding leaves scope. In Rust, Drop handles cleanup when the sandbox goes out of scope. In Go, pair every CreateSandbox with a defer that calls Stop + Close.

<CodeGroup> ```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();

}


```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
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())
ruby
Microsandbox::Sandbox.with("temp", image: "python", replace: true) do |sandbox|
  output = sandbox.exec("python", ["-c", "print('hello')"])
  puts output.stdout
end
</CodeGroup>