Skip to main content
Code running inside a sandbox can fail — especially AI-generated code. This tutorial covers every error-handling pattern you need: checking exit codes, catching typed exceptions, handling timeouts, and streaming stderr for real-time debugging.

What you’ll learn

  1. Check exit codes and stderr from command results
  2. Catch BoxliteError and other exceptions for infrastructure errors
  3. Handle timeouts safely so long-running commands don’t block forever
  4. Stream stderr in real time using the low-level execution API

Prerequisites

pip install boxlite
Requires Python 3.10+.

Step 1: Check exit codes

Every exec() call returns a result with an exit code, stdout, and stderr. The exec() method does not raise an exception for non-zero exit codes — you need to check them yourself.
exit_codes.py
import asyncio
import boxlite


async def main():
    async with boxlite.SimpleBox(image="python:slim") as box:
        # A successful command
        result = await box.exec("echo", "hello")
        print(f"Exit code: {result.exit_code}")  # 0
        print(f"Stdout: {result.stdout}")         # "hello\n"

        # A failing command — exec() does NOT raise, it returns the error info
        result = await box.exec(
            "python", "-c", "import nonexistent_module"
        )
        print(f"Exit code: {result.exit_code}")  # 1
        print(f"Stderr: {result.stderr}")         # "...ModuleNotFoundError: ..."

        # Pattern: always check before using output
        result = await box.exec("python", "-c", "print(2 + 2)")
        if result.exit_code == 0:
            print(f"Result: {result.stdout.strip()}")
        else:
            print(f"Failed: {result.stderr}")


if __name__ == "__main__":
    asyncio.run(main())

ExecResult fields

FieldPythonNode.jsDescription
Exit coderesult.exit_coderesult.exitCode0 = success, non-zero = failure
Stdoutresult.stdoutresult.stdoutStandard output as string
Stderrresult.stderrresult.stderrStandard error as string
Error messageresult.error_messageN/ADiagnostic message when process died unexpectedly (Python only)

Step 2: Catch infrastructure exceptions

While exec() returns non-zero exit codes without raising, BoxLite does raise exceptions for infrastructure failures — invalid images, command-not-found, config errors, timeouts, and more. The exception hierarchy is:
BoxliteError (base)
├── ExecError       — execution infrastructure failure
├── TimeoutError    — operation exceeded time limit
└── ParseError      — failed to parse command output
exceptions.py
import asyncio
from boxlite import SimpleBox, BoxliteError, TimeoutError


async def main():
    # Catch infrastructure errors (bad image, command not found, etc.)
    try:
        async with SimpleBox(image="python:slim") as box:
            # This raises RuntimeError — command doesn't exist in the image
            result = await box.exec("nonexistent_command")

    except RuntimeError as e:
        # Command-not-found raises RuntimeError
        print(f"Command not found: {e}")

    except TimeoutError:
        # An operation timed out (e.g. wait_until_ready)
        print("Operation timed out")

    except BoxliteError as e:
        # Catch-all for BoxLite errors (image pull, config, etc.)
        print(f"BoxLite error: {e}")

    # For exec() with valid commands, check exit codes instead
    async with SimpleBox(image="python:slim") as box:
        result = await box.exec("python", "-c", "import sys; sys.exit(42)")
        # No exception — check the exit code
        if result.exit_code != 0:
            print(f"Command exited with code {result.exit_code}")


if __name__ == "__main__":
    asyncio.run(main())
Key distinction: exec() returns non-zero exit codes as data (check result.exit_code). It only raises exceptions for infrastructure failures like command-not-found, which means the command couldn’t be started at all.

Step 3: Handle timeouts safely

When a command runs too long, use asyncio.wait_for() (Python) or Promise.race() (Node.js) to set a deadline. The guest process may keep running inside the VM after a timeout, but it will be cleaned up when the box shuts down.
timeout.py
import asyncio
from boxlite import SimpleBox


async def main():
    async with SimpleBox(image="python:slim") as box:
        try:
            # Wait with a 5-second timeout
            result = await asyncio.wait_for(
                box.exec("sleep", "3600"),
                timeout=5.0
            )
            print(f"Completed with exit code: {result.exit_code}")
        except asyncio.TimeoutError:
            print("Command timed out")

        # The box is still usable after a timeout
        result = await box.exec("echo", "still alive")
        print(f"After timeout: {result.stdout.strip()}")


if __name__ == "__main__":
    asyncio.run(main())

Timeout with a helper function

For repeated use, wrap the pattern in a helper.
timeout_helper.py
import asyncio
from boxlite import SimpleBox


async def exec_with_timeout(box, timeout_seconds, cmd, *args):
    """Run a command with a timeout. Returns ExecResult or None if timed out."""
    try:
        return await asyncio.wait_for(
            box.exec(cmd, *args),
            timeout=timeout_seconds
        )
    except asyncio.TimeoutError:
        return None


async def main():
    async with SimpleBox(image="python:slim") as box:
        # This completes in time
        result = await exec_with_timeout(box, 10, "echo", "fast")
        if result:
            print(f"Output: {result.stdout.strip()}")

        # This times out
        result = await exec_with_timeout(box, 2, "sleep", "3600")
        if result is None:
            print("Command timed out")


if __name__ == "__main__":
    asyncio.run(main())

Advanced: kill the guest process explicitly

If you need to kill the timed-out process instead of letting it run until the box shuts down, use the low-level execution API.
timeout_kill.py
import asyncio
from boxlite import Boxlite, BoxOptions


async def main():
    runtime = Boxlite.default()
    box = await runtime.create(BoxOptions(image="python:slim"))
    try:
        # Low-level exec returns an Execution handle
        execution = await box.exec("sleep", ["3600"])

        try:
            result = await asyncio.wait_for(execution.wait(), timeout=5.0)
            print(f"Completed: {result.exit_code}")
        except asyncio.TimeoutError:
            # Kill the guest process explicitly
            await execution.kill()
            print("Timed out — process killed")
    finally:
        await box.stop()


if __name__ == "__main__":
    asyncio.run(main())

Step 4: Stream stderr in real time

For long-running commands, you might want to see errors as they happen instead of waiting for the command to finish. Use the low-level execution API with async iterators.
The high-level SimpleBox.exec() collects all output and returns it as a single string. For real-time streaming, use the low-level API via Boxlite.default() and runtime.create().
stream_stderr.py
import asyncio
from boxlite import Boxlite, BoxOptions


async def main():
    runtime = Boxlite.default()
    box = await runtime.create(BoxOptions(image="python:slim"))
    try:
        # Low-level exec gives you streaming access
        execution = await box.exec("python", ["-c", """
import sys
import time

for i in range(5):
    print(f"Progress: {i+1}/5")
    sys.stdout.flush()
    if i == 2:
        print("Warning: something looks off", file=sys.stderr)
        sys.stderr.flush()
    time.sleep(1)

print("Done!")
"""])

        # Stream stdout and stderr concurrently
        async def read_stdout():
            async for line in execution.stdout():
                print(f"[stdout] {line}", end="")

        async def read_stderr():
            async for line in execution.stderr():
                print(f"[stderr] {line}", end="")

        await asyncio.gather(read_stdout(), read_stderr())

        result = await execution.wait()
        print(f"Exit code: {result.exit_code}")
    finally:
        await box.stop()


if __name__ == "__main__":
    asyncio.run(main())
Each stream (stdout, stderr) can only be iterated once. After iteration, the stream is consumed.

Debugging tips

Enable debug logging

Set the RUST_LOG environment variable to see detailed BoxLite internals — VM lifecycle, image pulls, command execution, and network setup.
# See everything
RUST_LOG=debug python my_script.py

# Filter to BoxLite only
RUST_LOG=boxlite=debug python my_script.py

Check box status

If commands fail unexpectedly, check whether the box is still running.
info = await box.info()
print(f"Box status: {info.status}")  # "running", "stopped", etc.

Common failure patterns

SymptomLikely causeFix
exit_code: 127Command not found in imageUse an image that has the command, or install it first
exit_code: 137Process killed (OOM or SIGKILL)Increase memory_mib
exit_code: -9Process terminated by signalCheck if another process or timeout killed it
error_message is setVM or container init failedEnable debug logging and check the message
BoxliteError: image errorImage pull failedCheck network, verify image name/tag exists
RuntimeError: spawn_failedCommand binary not in imageCheck the image has the binary, or install it

What’s next?