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:
- The error is caught by the worker infrastructure
- The error message and stack trace are serialized
- The serialized error is sent back to the host
- The host reconstructs the error and throws it
This means you can use standard try/catch patterns in your host code:
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:
/**
* 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:
/**
* 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:
/**
* 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:
// 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:
// 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:
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):
type DivideResult =
| { success: true; value: number }
| {
success: false;
error: string;
code: 'DIVISION_BY_ZERO' | 'INVALID_INPUT';
};See Also
- {% example-link error-handling %} - Complete error handling example
- API Reference: createWorker - Worker creation options