> ## Documentation Index
> Fetch the complete documentation index at: https://microsanbox-staging-toks-cloud-snapshot-contracts.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Error handling

> Typed errors and resource cleanup patterns

Rust, TypeScript, Python, and Go surface typed errors so you can match 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`. Ruby currently exposes `Microsandbox::Error`; where no dedicated subclass exists, its reference documents the stable message contract explicitly.

## Matching errors

<CodeGroup>
  ```rust Rust theme={null}
  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),
  }
  ```

  ```typescript TypeScript theme={null}
  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;
      }
  }
  ```

  ```python Python theme={null}
  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 Go theme={null}
  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 Ruby theme={null}
  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. 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 theme={null}
  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 theme={null}
  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 theme={null}
  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 theme={null}
  // 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 theme={null}
  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 theme={null}
  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 theme={null}
  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 theme={null}
  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 Ruby theme={null}
  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-idiomatic equivalents to converge on the existing persisted identity 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 identity. See [Naming conflicts](/sandboxes/overview#naming-conflicts) for the grace-period knob.

## Stale receiver identities

Built-in local and cloud lifecycle receivers capture a stable sandbox identity in addition to the reusable name. If the name now points to a replacement, receiver lifecycle operations refuse to act on it and surface a typed stale-identity error.

<CodeGroup>
  ```rust Rust theme={null}
  match stale_handle.destroy().await {
      Err(Error::SandboxReplaced { expected, actual, .. }) => {
          eprintln!("refusing stale operation: {expected} -> {actual}");
      }
      result => result?,
  }
  ```

  ```typescript TypeScript theme={null}
  import { SandboxReplacedError } from "microsandbox";

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

  ```python Python theme={null}
  from microsandbox import SandboxReplacedError

  try:
      await stale_handle.destroy()
  except SandboxReplacedError:
      pass
  ```

  ```go Go theme={null}
  if err := staleHandle.Destroy(ctx); m.IsKind(err, m.ErrSandboxReplaced) {
      log.Println("refusing stale lifecycle operation")
  }
  ```

  ```ruby Ruby theme={null}
  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 (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 theme={null}
  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 theme={null}
  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 theme={null}
  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 theme={null}
  // 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 theme={null}
  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 theme={null}
  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 theme={null}
  # 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 theme={null}
  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>
