Failures
execute resolves when the program failed and rejects when the executor did. A program failure is the ordinary case. From your host’s point of view the call succeeded: the model wrote something that did not work, and it can fix it.
| Program failure | Executor fault | |
|---|---|---|
| How it arrives | execute resolves, and the text is in outcome.error |
execute rejects |
| What broke | the code the model wrote | something under the execution |
| What you get | text written for the model to act on | a code and a retryable flag |
| What you do | show the text to the model and let it try again | branch on the code, and retry while retryable is true |
Your host tells the two apart in a single branch:
const outcome = await executor.execute({ code }) // rejects only for infrastructure
if (outcome.error !== undefined) { // the program failed; show this text to the model and let it try again}Every way a program can fail arrives in outcome.error: a syntax error, a throw, a rejected promise, a bound the executor enforced, and a refusal the program could have caught and did not. Nothing a program writes makes execute reject.
Reading a fault
Section titled “Reading a fault”Only the message, code and retryable cross a boundary. The class does not, and the executor builds it back on your side: a fault carrying a code arrives as ExecutorError or one of its subclasses, in either arrangement, and a fault the child composed without one, such as the handle-not-found text, arrives as a plain Error. Branch on the code. rehydrateExecutorError is what lets that one branch cover everything reaching the catch, since a provider rejection and a throw from your own code land there too:
import { ExecutorError, rehydrateExecutorError } from '@supolka/codemode-executor'
try { return await executor.execute({ code })} catch (failure) { const fault = rehydrateExecutorError(failure)
if (fault instanceof ExecutorError && fault.retryable) { await wait(backoff)
return await executor.execute({ code }) }
throw fault}rehydrateExecutorError rebuilds the typed error in your own process from the code that crossed the wire, and passes through untouched anything whose code it does not recognize. Provider rejections and throws from your own code keep their shape. Do not drop the wait. A queue that was full the instant it rejected is still full an instant later. Every fault has a retryable flag, and it says whether trying again can work at all. Read it instead of keeping a table of which codes deserve a second attempt.
Fault codes, and what each one asks of you
Section titled “Fault codes, and what each one asks of you”Read the flag to decide whether to try again, and the code to decide what to change:
| Code | What it means | What to do |
|---|---|---|
QUEUE_OVERFLOW |
Every worker is busy and the waiting line is at its bound. | Back off and retry. Scale on the queue depth. |
WORKER_CRASHED |
The child process under the execution died or never started. | Retry once. The pool has already replaced the worker. |
CONNECTION_BROKEN |
The transport under a call died, or the executor is disposed. | Read retryable: a broken session reopens on the next call, a disposed executor does not. |
CONNECTION_REJECTED |
connect failed on the socket or on the handshake. |
Check the address and whatever admits the connection. |
PROTOCOL_MISMATCH |
The two sides disagree on the protocol version. | Deploy matching versions. Reconnecting never heals it. |
RESPONSE_TOO_LARGE |
The server measured the answer above its response limit. | Return less, or raise the limit on the server. |
HANDLES_CLOSED |
The execution named by invokeHandle or closeHandles holds no handles. |
Execute again with retainHandles for a fresh executionId. |
INTERNAL |
An invariant of the executor broke, or the pool is shutting down. | Read retryable. Work the pool had accepted and a shutdown cut before it started goes to another replica. Report the rest: no change to the request makes it go away. |
Hosts meet two of them in production, and both are always retryable:
ExecutorError | QUEUE_OVERFLOW | retryable true | The queue is at its bound of 1. Retry when a running execution settles.ExecutorError | WORKER_CRASHED | retryable true | Worker 1 failed to start.Two more are worth recognizing on sight, because they mean a wiring problem and not a load problem. Connecting to an address with no listener gives CONNECTION_REJECTED | retryable false | Executor handshake failed for ws://127.0.0.1:9. Calling a disposed executor gives INTERNAL | The pool is shutting down. in your own process and CONNECTION_BROKEN | RemoteExecutor is closed over the wire. The failure reference enumerates every code beside the file it is raised in. It also lists the fault classes whose text is written to the same grammar as the declared failures below.
Texts a model reads
Section titled “Texts a model reads”Declared failures are written for the model and not for a log. Each one names what happened in its first sentence and says what remains possible in the next. That second sentence turns a failed run into a better next attempt and not a loop:
Execution ran out of memory at its 32 MB limit. Work through the data in smaller pieces and keep only what you return.
Show that sentence to the model. The 32 MB in it is the limit the execution actually ran under. Every size in one of these texts is grouped for one locale, so the sentence reads the same wherever the executor runs. The failure reference quotes each of them beside the class that composes it. Where a text arrives depends on which boundary reported it. Anything that failed before the program ran, and any bound the executor enforced, arrive in the envelope’s error. Something the program itself touched arrives as a rejection it can catch, so the model’s own try can work around it:
| Where it arrives | What the program catches |
|---|---|
The envelope’s error |
A timeout, the memory limit, a provider name, an unmapped import |
A TypeError from fetch |
Outbound disabled, and an outbound request past the payload bound |
A DOMException named NotSupportedError |
A declared absence such as caches |
A plain Error at a tool call site |
The not-found text, the completed text, a tool call past the payload bound |
Declared absences a program can catch
Section titled “Declared absences a program can catch”Names this runtime does not have are usually absent completely: typeof reads undefined, and a program that checks for a feature takes the branch it already has. But a few names exist and fail at their first touch instead, each with a sentence saying why. A bare undefined there would send the model hunting for a bug that does not exist.
await executor.execute({ code: `async () => { try { await caches.open('x') } catch (failure) { return { name: failure.name, message: failure.message } } }`,}){ result: { name: 'NotSupportedError', message: 'Cache storage is absent from this runtime because nothing survives an execution. Return what should persist in the result and take it back as an input on the next call.' }, logs: []}caches is the entry point to the Cache API, and this runtime defines it only to explain its own absence. CompressionStream, DecompressionStream and the two buffer growth operations fail the same way, each with its own reason. Runtime deviations lists every one.
Failures your host causes
Section titled “Failures your host causes”| Text | What your code did |
|---|---|
The dispatch answered outside the result envelope. |
Your dispatch returned something that is not an object at all. |
The fetch handle answered with a value that is not a Response. |
A retained handler returned something other than a Response. |
The fetch handle answered outside the response wire shape. |
A retained handler’s answer failed the check the host runs on the serialized Response. |
| A provider name text | Your request named a provider the entry scope refuses. The text arrives in outcome.error before a single line of the program runs. |
The envelope check refuses only a value that is not an object. An object of the wrong shape gets through: the proxy reads result off it, so the call returns undefined and reports no failure. Use createEnvelopeDispatch and neither happens. Invoking a handle says what a handler has to return, and the tools guide says which provider names are refused. Where names come from user data, validate them before the call, or read the text and rename.
Failures a program cannot cause
Section titled “Failures a program cannot cause”User-code failures never throw on your side, and no program can make your host’s execute reject by writing bad code. If one does, that is a defect in this executor and not in the program. Unhandled rejections inside the isolate do not replace the result a program already reported:
await executor.execute({ code: `async () => { Promise.reject(new Error('ignored')); return 'kept' }`,})// { result: 'kept', logs: [] }Tool calls that arrive after the run settled do not reach your data. The sandbox composes Execution has already completed. without entering your dispatch, and the state latch the tools guide shows closes that same path a second time. A program cannot end the worker it runs in by returning too much. Each of those is a boundary with a corpus attacking it on every run of pnpm verify, described in the threat model.
This page tells a program’s failure from the executor’s. Modules ships a program what it needs to import, and Handles lets one keep serving after it returns.