Skip to content

Error Hierarchy and Control Flow

Sealed Error Hierarchy

All interpreter errors flow through the sealed MontyError hierarchy. Consumers use exhaustive pattern matching:

try {
  await platform.run(code);
} on MontyError catch (e) {
  switch (e) {
    case MontyScriptError(:final exception):
      // Python exception — e.message, e.excType, e.exception (full details)
    case MontyPanicError():
      // Rust panic — interpreter bug, harsh backoff
    case MontyCrashError():
      // Isolate/Worker died — restart immediately
    case MontyDisposedError():
      // Disposed while running — fix the caller
    case MontyResourceError():
      // OOM, timeout, WASM trap — reduce limits, retry
  }
}

Type Relationships

Exception (dart:core)
  ├── MontyError (sealed)           ← thrown by platform on failure
  │     ├── MontyScriptError        ← Python exceptions (wraps MontyException)
  │     ├── MontyPanicError         ← Rust catch_unwind
  │     ├── MontyCrashError         ← Isolate/Worker death
  │     ├── MontyDisposedError      ← disposed during execution
  │     └── MontyResourceError      ← MemoryLimitExceeded, timeout, WASM trap
  │
  └── MontyException (standalone)   ← data class for Python exception details
        fields: message, excType, filename, lineNumber, columnNumber,
                sourceCode, traceback: List<MontyStackFrame>

MontyScriptError.exception carries the full MontyException with traceback, source location, and Python exception type. The message and excType fields are duplicated on MontyScriptError for convenience.

Error Source to Sealed Type Mapping

Source excType Sealed type thrown
Python raise ValueError(...) ValueError MontyScriptError
Python 1/0 ZeroDivisionError MontyScriptError
Python syntax error SyntaxError MontyScriptError
Any Python exception * (except below) MontyScriptError
Memory limit exceeded MemoryLimitExceeded MontyResourceError
Timeout exceeded TimeoutError MontyResourceError
WASM trap (stack overflow) WasmTrap MontyResourceError
Rust panic (catch_unwind) -- MontyPanicError
Isolate/Worker unexpected exit -- MontyCrashError
Disposed during execution -- MontyDisposedError
Calling run() while active -- StateError (not MontyError)

Control Flow Through Boundaries

Python raises ZeroDivisionError
  │
  ▼
Rust monty crate catches → RunProgress::Error { exception JSON }
  │
  ▼ (FFI path)                           ▼ (WASM path)
NativeBindingsFfi reads                  Worker postMessage
  error JSON from C out-param              { ok: false, error, excType,
  │                                          filename, lineNumber, ... }
  ▼                                        │
FfiCoreBindings._translateError            ▼
  → CoreProgressResult(state:'error')    WasmCoreBindings._translateProgressResult
  │                                        → CoreProgressResult(state:'error')
  ▼                                        │
BaseMontyPlatform.translateProgress        ▼
  │                                      BaseMontyPlatform.translateProgress
  ▼                                        │
_throwError(message, excType, ...)         ▼
  │                                      _throwError(message, excType, ...)
  ├─ excType == 'MemoryLimitExceeded'      │
  │   → throw MontyResourceError           ├─ (same mapping)
  │                                        │
  └─ all other excTypes                    └─ all other excTypes
      → build MontyException(...)              → build MontyException(...)
      → throw MontyScriptError(                → throw MontyScriptError(
          message, excType, exception)             message, excType, exception)

Propagation Through Isolate Boundary

Background Isolate catches during execution:
  │
  ├─ on MontyScriptError → _ErrorResponse(id, error)
  │                          main thread rethrows as MontyScriptError
  │
  ├─ on MontyError → _MontyErrorResponse(id, error)
  │                    main thread rethrows the sealed subtype
  │
  └─ on Object → _GenericErrorResponse(id, message)
                   main thread throws StateError(message)

Isolate exits unexpectedly:
  → _failAllPending('Isolate exited')
    → all pending completers receive StateError

Propagation Through Bridge

MontyBridge (DefaultMontyBridge._run) catches:
  │
  ├─ on MontyScriptError catch (e)
  │   → log warning
  │   → flush print buffer → emit BridgeTextStart/Content/End
  │   → emit BridgeRunError(
  │       message: e.exception!.message,
  │       exception: e.exception,
  │       printOutput: buffered output)
  │
  ├─ on MontyError catch (e)
  │   → log warning
  │   → emit BridgeRunError(message: e.message)
  │
  └─ on Object catch (e, st)
      → log error with stack trace
      → emit BridgeRunError(message: '$e')

MontyResult.error vs Thrown Errors

A successful run() returns MontyResult which may carry a non-fatal MontyException in its error field (e.g., a SyntaxWarning that didn't prevent execution). A failed run() throws a sealed MontyError -- it never returns a MontyResult.

platform.run(code)
  │
  ├─ ok: true  → return MontyResult(value: ..., error: MontyException?)
  │              (error may be non-null for warnings)
  │
  └─ ok: false → throw MontyScriptError / MontyResourceError
                  (never returns a MontyResult)

Resource Safety: NativeFinalizer

FfiCoreBindings attaches a NativeFinalizer backed by monty_free() to every active Rust handle. If a MontyFfi instance is garbage collected without dispose(), the finalizer automatically frees the handle, preventing a permanent native memory leak. The finalizer is detached on explicit _freeHandle() or dispose() calls to avoid double-free.