docs/sdk/errors.mdx
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.
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
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;
}
}
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")
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)
}
}
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
exec() distinguishes between:
ExecOutput with a non-zero code. This is not an error in the SDK sense; it's a normal result.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.
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;
}
}
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
// 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
}
}
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.
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.).
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;
}
}
from microsandbox import SandboxAlreadyExistsError
try:
sb = await Sandbox.create("worker", image="alpine")
except SandboxAlreadyExistsError:
print("sandbox already exists; resume or pass replace=True")
sb, err := m.CreateSandbox(ctx, "worker",
m.WithImage("alpine"))
if m.IsKind(err, m.ErrSandboxAlreadyExists) {
log.Println("sandbox already exists; resume or pass WithReplace()")
}
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
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 for the grace-period knob.
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 match stale_handle.destroy().await { Err(Error::SandboxReplaced { expected, actual, .. }) => { eprintln!("refusing stale operation: {expected} -> {actual}"); } result => result?, } ```import { SandboxReplacedError } from "microsandbox";
try {
await staleHandle.destroy();
} catch (error) {
if (!(error instanceof SandboxReplacedError)) throw error;
}
from microsandbox import SandboxReplacedError
try:
await stale_handle.destroy()
except SandboxReplacedError:
pass
if err := staleHandle.Destroy(ctx); m.IsKind(err, m.ErrSandboxReplaced) {
log.Println("refusing stale lifecycle operation")
}
begin
stale_handle.destroy
rescue Microsandbox::Error => error
raise unless error.message.include?("was replaced")
end
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.
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.
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;
}
}
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.")
// 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)
}
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.
<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.
// 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();
}
# 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)
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())