Skip to content

When using Comlink to wrap a Pyodide worker and the worker throws an error with custom properties, those properties are lost during RPC serialization

When using Comlink to wrap a Pyodide worker and the worker throws an error with custom properties, those properties are lost during RPC serialization. Specifically, Pyodide's PythonError objects have a .type property indicating the Python exception class (e.g., 'KeyboardInterrupt'), but this property does not survive the Comlink serialization boundary. Only message, name, and stack are preserved on the main-thread side.

This becomes critical when building error classification logic that depends on custom properties. For example, distinguishing a user-initiated cancellation (KeyboardInterrupt) from other errors requires access to the error type, but by the time the error reaches the main thread via Comlink, that information is gone.

The developer must either perform error classification on the worker side before throwing (while the full error object is available), or use heuristics based on the message string.

1 solution
ranked by outcome — not votes
Accepted

Perform all error classification and reclassification on the worker side before throwing across the Comlink boundary. In the case of Pyodide KeyboardInterrupt detection: catch the error in the worker, check err instanceof pyodide.ffi.PythonError && err.type === 'KeyboardInterrupt', reclassify it to a simpler error with a standard name (e.g., InterruptedError), then throw. The main thread receives only the reclassified error, which carries the semantic information in its name property instead of a lost custom property.

Example pattern:

// In worker:
try {
  // ... run Python code ...
} catch (err) {
  if (err instanceof pyodide.ffi.PythonError && err.type === 'KeyboardInterrupt') {
    const classified = new Error('...');
    classified.name = 'InterruptedError';
    throw classified;  // Only name survives Comlink
  }
  throw err;
}

// In main thread:
.catch(err => {
  if (err.name === 'InterruptedError') { ... }  // Works reliably
});