Cancellation

Stop an active run and propagate cancellation to host functions.

Cancellation

Cancellation stops work the host no longer needs: a client disconnected, a user cancelled the operation, or the parent request ended before the sandbox returned a result.

Concept

run uses the standard AbortSignal API. The host creates an AbortController and passes its signal to run() or runner.run(). Calling abort() rejects the invocation with RunAbortedError.

The same cancellation state is exposed to every active host function through getHostFunctionContext().abortSignal. A host function must pass that signal to its own cancellable operations. The runtime cannot automatically stop a database query, HTTP request, or application task that ignores the signal.

Cancellation stops the current invocation. It does not reverse a host side effect that already completed.

Implementation

Create an AbortController, pass its signal to the run, and retain the controller for the lifetime of the host request:

import { run } from 'run';

const controller = new AbortController();

const resultPromise = run({
  source: `
    const result = await search.query("cancellation");
    return result;
  `,
  hostFunctions,
  abortSignal: controller.signal,
});

// Call this when the host request is cancelled.
controller.abort();

Host functions should propagate the signal returned by getHostFunctionContext():

import { getHostFunctionContext } from 'run';

const hostFunctions = {
  search: {
    query: async (query: string) => {
      const { abortSignal } = getHostFunctionContext();
      const response = await fetch(
        `https://api.example.com/search?q=${encodeURIComponent(query)}`,
        { signal: abortSignal },
      );

      return response.json();
    },
  },
};

The fetch() call runs in the host, not in the sandbox. When the run is cancelled, the host function receives the abort event and the request can stop without waiting for the remote server.

Handle cancellation by checking the stable package error code:

import { RunError } from 'run';

try {
  const result = await resultPromise;
  // Handle the completed or interrupted result.
} catch (error) {
  if (RunError.isInstance(error) && error.code === 'RUN_ABORTED') {
    // The caller intentionally cancelled this invocation.
  } else {
    throw error;
  }
}

Requests

In a server handler, connect the request signal to the run signal so that work stops when the request is abandoned. This example assumes the source does not use interruptions:

export async function POST(request: Request) {
  const result = await run({
    source,
    hostFunctions,
    abortSignal: request.signal,
  });

  return Response.json(result);
}

If an application combines several cancellation sources, it can use AbortSignal.any():

const signal = AbortSignal.any([request.signal, applicationShutdown.signal]);

const result = await run({
  source,
  hostFunctions,
  abortSignal: signal,
});

Timeouts

Cancellation and timeouts both stop an invocation, but they report different errors. An explicit abort produces RUN_ABORTED, while exceeding timeoutMs produces RUN_TIMEOUT.

The timeout covers guest execution, host function work, and continuation operations. Both outcomes abort the signal exposed to active host functions.

Use timeoutMs as the runtime budget and an abort signal for lifecycle events such as client disconnects and application shutdown:

const result = await run({
  source,
  hostFunctions,
  abortSignal: request.signal,
  limits: {
    timeoutMs: 10_000,
  },
});

Side effects

An aborted host function may have completed external work before it observed cancellation. For example, a payment may be accepted immediately before the signal is aborted.

Host functions that perform writes should use idempotency keys or application-level transactions. Cancellation is a way to stop unnecessary work, not a rollback mechanism.