> ## Documentation Index
> Fetch the complete documentation index at: https://docs.microsandbox.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Error handling

> Handle sandbox errors and clean up resources across the microsandbox SDKs.

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, and Python provides dedicated exception classes. Go provides an `*Error` value with an `ErrorKind` discriminator that you match via `m.IsKind(err, kind)` or `errors.As`. Ruby v0.7.4 adds typed subclasses of `Microsandbox::Error` with stable `code` strings; failures without a dedicated subclass still use the base class.

## Matching errors

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

  ```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),
  }
  ```

  ```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::ExecTimeoutError => error
    warn "Timed out: #{error.message}"
  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 theme={null}
  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 Rust theme={null}
  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 Python theme={null}
  from microsandbox import MicrosandboxError

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

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

  ```ruby Ruby theme={null}
  require "microsandbox"

  begin
    output = sandbox.exec("nonexistent")
    # If the program ran, check output.success? and output.exit_code.
  rescue Microsandbox::Error => e
    warn "Program could not be started: #{e.message}"
  end
  ```
</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. Branch on it to decide how to recover, such as resuming the existing sandbox or regenerating the name.

<CodeGroup>
  ```typescript TypeScript theme={null}
  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 theme={null}
  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 Python theme={null}
  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 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::SandboxAlreadyExistsError
    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](/sandboxes/overview#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 theme={null}
  import { SandboxReplacedError } from "microsandbox";

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

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

  ```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.

## Stop observation timeouts

On Local, a graceful-stop timeout causes the SDK to force-kill the sandbox. On Cloud, the same timeout only limits how long the SDK waits: the accepted server-side stop is not cancelled and may still complete. Cloud reports this with `SandboxStopTimedOutError` in TypeScript and Python, `Error::SandboxStopTimedOut` in Rust, and `ErrSandboxStopTimedOut` in Go. Ruby currently raises `Microsandbox::Error` with a message explaining that the accepted stop may still complete.

After a Cloud timeout, poll the sandbox status or call the wait-until-stopped method if you still need confirmation. Calling `request_stop` first is useful when the application wants to submit the stop and control its own observation deadline.

## 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 theme={null}
  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 theme={null}
  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 Python theme={null}
  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 Go theme={null}
  _, err := m.CreateSandbox(ctx, "svc", m.WithImage("alpine"))
  if err != nil {
      log.Printf("sandbox failed to start: %v", err)
  }
  ```

  ```ruby Ruby theme={null}
  require "microsandbox"

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

Startup errors include guest initialization failures, such as a user missing from the image.

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

  ```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
  ```

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

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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.