Home/Error Handling

Error Handling in isolated-workers

When a worker throws an error, isolated-workers automatically serializes it and re-throws it in the host process. This guide explains how error propagation works and best practices for handling errors.

How Error Propagation Works

When you call worker.sendRequest(), the message is sent to the worker process over IPC. If the worker's handler throws an error:

  1. The error is caught by the worker infrastructure
  2. The error message and stack trace are serialized
  3. The serialized error is sent back to the host
  4. The host reconstructs the error and throws it

This means you can use standard try/catch patterns in your host code:

typescript
try {
  const result = await worker.send('divide', { a: 10, b: 0 });
} catch (error) {
  console.error('Worker threw an error:', error.message);
}

Example: Division with Error Handling

Here's a complete example showing error propagation. First, the shared message definitions:

messages.ts
/**
 * Shared message definitions for the error-handling example
 *
 * This file is imported by both the host and worker to ensure
 * type safety and avoid duplication.
 */

import { DefineMessages } from 'isolated-workers';

/**
 * Message types for the division example
 */
export type Messages = DefineMessages<{
  divide: {
    payload: { a: number; b: number };
    result: { result: number };
  };
}>;

The worker validates input and throws for invalid operations:

worker.ts
/**
 * Error Handling Example - Worker (Server) Side
 *
 * Demonstrates error throwing and propagation.
 */

import { startWorkerServer, Handlers } from 'isolated-workers';
import type { Messages } from './messages.js';

// Define handlers for incoming messages with proper typing
const handlers: Handlers<Messages> = {
  divide: ({ a, b }) => {
    console.log(`Worker: dividing ${a} / ${b}`);

    if (b === 0) {
      throw new Error('Division by zero');
    }

    return { result: a / b };
  },
};

async function main() {
  console.log('Worker: starting error-handling demo worker');

  await startWorkerServer(handlers);

  console.log('Worker: ready');

  process.on('SIGTERM', () => {
    console.log('Worker: shutting down');
    process.exit(0);
  });
}

main().catch((err) => {
  console.error('Worker error:', err);
  process.exit(1);
});

The host catches and handles the error:

host.ts
/**
 * Error Handling Example - Host (Client) Side
 *
 * Demonstrates error propagation from worker to host.
 */

import { createWorker } from 'isolated-workers';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import type { Messages } from './messages.js';

const __dirname = dirname(fileURLToPath(import.meta.url));

async function main() {
  console.log('=== Error Handling Example ===\n');

  const worker = await createWorker<Messages>({
    script: join(__dirname, 'worker.ts'),
    timeout: 10000,
  });

  console.log(`Worker spawned with PID: ${worker.pid}\n`);

  // Test 1: Successful division
  console.log('Test 1: 10 / 2');
  try {
    const result = await worker.send('divide', { a: 10, b: 2 });
    console.log('Result:', result.result, '\n');
  } catch (err) {
    console.error('Unexpected error:', (err as Error).message, '\n');
  }

  // Test 2: Division by zero (should error)
  console.log('Test 2: 10 / 0 (should error)');
  try {
    await worker.send('divide', { a: 10, b: 0 });
    console.log('ERROR: Should have thrown!\n');
  } catch (err) {
    console.log('Caught expected error:', (err as Error).message, '\n');
  }

  // Test 3: Cleanup
  console.log('Test 3: Cleanup');
  await worker.close();
  console.log('Worker closed successfully\n');

  console.log('=== All tests passed ===');
}

main().catch((err) => {
  console.error('Host error:', err);
  process.exit(1);
});

Best Practices

1. Use Descriptive Error Messages

Since errors cross process boundaries, make your error messages descriptive:

typescript
// Good - clear context
throw new Error(`Division by zero: cannot divide ${a} by ${b}`);

// Bad - vague
throw new Error('Invalid input');

2. Handle Errors at the Right Level

Catch errors where you can meaningfully handle them:

typescript
// Handle specific operations
try {
  const result = await worker.sendRequest({ type: 'riskyOperation' });
  return result;
} catch (error) {
  // Log and provide fallback
  console.error('Risky operation failed, using default');
  return defaultValue;
}

3. Validate Early

Validate inputs in the worker before performing operations:

typescript
handlers: {
  divide: ({ a, b }) => {
    if (typeof a !== 'number' || typeof b !== 'number') {
      throw new Error('Both arguments must be numbers');
    }
    if (b === 0) {
      throw new Error('Cannot divide by zero');
    }
    return a / b;
  };
}

Error Types

Currently, isolated-workers preserves:

  • Error message (error.message)
  • Error type name (error.name)
  • Error stack trace (error.stack)
  • Error code for Node.js errors (error.code)

Custom error properties may not be preserved across the process boundary. If you need to pass structured error data, consider returning an error result instead of throwing (note: this example uses a different pattern than the division example above):

typescript
type DivideResult =
  | { success: true; value: number }
  | {
      success: false;
      error: string;
      code: 'DIVISION_BY_ZERO' | 'INVALID_INPUT';
    };

See Also