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

> Typed errors and resource cleanup patterns

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 theme={null}
  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 theme={null}
  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 theme={null}
  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 theme={null}
  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 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()")
  }
  ```
</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 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

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>
