Unexpected Shutdown Handling
Workers can crash unexpectedly due to out-of-memory errors, SIGKILL signals, or other hard crashes. This guide explains how isolated-workers detects crashes, configures recovery strategies, and handles pending operations.
What is Unexpected Shutdown?
An unexpected shutdown occurs when a worker process terminates without going through the normal close procedure:
Common causes:
- Out-of-memory (OOM) crashes - Worker exceeds memory limits
- SIGKILL - Process forcibly killed (e.g.,
kill -9) - Segmentation faults - Native code crashes
- Unhandled exceptions - Uncaught errors that terminate the process
Detection: isolated-workers listens to process exit events, which fire reliably for all crash types—including OOM kills that might not trigger socket errors.
Default Behavior
By default, pending operations are rejected immediately when a worker crashes unexpectedly:
import { createWorker } from 'isolated-workers';
const worker = await createWorker<MyMessages>({
script: './worker.js',
// unexpectedShutdown defaults to: { strategy: 'reject' }
});
try {
await worker.send('processData', { largeData });
} catch (error) {
// Worker crashed during processing - request is rejected
console.error('Worker crashed:', error.message);
}Configuration Strategies
isol-workers supports two strategies for handling unexpected shutdowns:
1. Reject Strategy (Default)
Immediately reject all pending operations when a crash is detected. Use this for non-idempotent operations where retrying could cause duplicate work or corruption.
const worker = await createWorker<MyMessages>({
script: './worker.js',
unexpectedShutdown: {
strategy: 'reject',
},
});2. Retry Strategy
Automatically retry failed operations up to a specified number of attempts. Use this for idempotent operations that can be safely retried.
const worker = await createWorker<MyMessages>({
script: './worker.js',
unexpectedShutdown: {
strategy: 'retry',
attempts: 3, // Optional: defaults to 1
},
});Per-Message-Type Overrides
Configure different strategies for different message types:
const worker = await createWorker<MyMessages>({
script: './worker.js',
unexpectedShutdown: {
strategy: 'reject', // Default for all message types
processData: { strategy: 'retry', attempts: 3 }, // Idempotent - retry up to 3 times
deleteRecord: { strategy: 'reject' }, // Non-idempotent - reject immediately
fetchRemote: { strategy: 'retry' }, // Uses default of 1 attempt
},
});Type-Safe Configuration
The UnexpectedShutdownConfig type provides autocomplete for your message types:
import { createWorker, type UnexpectedShutdownConfig } from 'isolated-workers';
const shutdownConfig: UnexpectedShutdownConfig<Messages> = {
strategy: 'reject',
processData: { strategy: 'retry', attempts: 3 }, // ← Autocomplete for your message types
safeQuery: { strategy: 'retry' },
};Error Handling
When a worker crashes, operations are rejected with a WorkerCrashedError containing rich context:
WorkerCrashedError Properties
class WorkerCrashedError extends Error {
readonly reason: ShutdownReason; // Why the worker crashed
readonly messageType: string; // Which message type was processing
readonly attempt: number; // Current attempt number
readonly maxAttempts: number; // Maximum configured attempts
}ShutdownReason Types
type ShutdownReason =
| { type: 'exit'; code: number | null; signal: string | null } // Process exit (code or signal)
| { type: 'error'; error: Error } // Socket/IPC error
| { type: 'close' }; // Connection closedAccessing Error Details
try {
await worker.send('processData', { data });
} catch (error) {
if (error instanceof WorkerCrashedError) {
console.error('Worker crashed:', error.message);
console.error('Processing:', error.messageType);
console.error('Attempt:', error.attempt, '/', error.maxAttempts);
// Access raw shutdown reason
if (error.reason.type === 'exit') {
console.error('Exit code:', error.reason.code);
console.error('Signal:', error.reason.signal); // e.g., 'SIGKILL'
}
}
}Error Messages
The error message provides context about the crash:
- Immediate reject:
"Worker crashed unexpectedly while processing 'processData'" - After retries exhausted:
"Worker crashed after 3 attempts while processing 'processData'" - With exit details:
"Worker crashed (exit code 137, signal SIGKILL) while processing 'processData'"
Best Practices
1. Use Retry for Idempotent Operations
Idempotent operations can be safely retried without side effects:
unexpectedShutdown: {
// Idempotent: Can retry safely
fetchUser: { strategy: 'retry', attempts: 3 },
validateInput: { strategy: 'retry', attempts: 2 },
// Non-idempotent: Reject to avoid duplicates
createPayment: { strategy: 'reject' },
sendNotification: { strategy: 'reject' },
deleteRecord: { strategy: 'reject' },
}2. Set Appropriate Retry Limits
Consider the cost and likelihood of success when setting retry limits:
unexpectedShutdown: {
// Quick operations: Higher retry count (transient failures)
healthCheck: { strategy: 'retry', attempts: 5 },
// Medium operations: Moderate retries
fetchFromCache: { strategy: 'retry', attempts: 3 },
// Slow operations: Lower retries (startup overhead)
processLargeFile: { strategy: 'retry', attempts: 2 },
}3. Log Shutdown Details for Debugging
Capture shutdown reasons to diagnose crash patterns:
try {
await worker.send('processData', { data });
} catch (error) {
if (error instanceof WorkerCrashedError) {
// Log for monitoring/alerting
logger.error('Worker crashed', {
messageType: error.messageType,
attempt: error.attempt,
maxAttempts: error.maxAttempts,
reason: error.reason,
});
// Check for OOM kills
if (error.reason.type === 'exit' && error.reason.code === 137) {
logger.warn('Possible OOM - consider increasing memory limits');
}
}
}4. Handle Expected vs Unexpected Shutdowns
The library distinguishes between expected and unexpected shutdowns:
- Expected shutdown: User calls
worker.close()→ Pending operations rejected with normal "Worker closed" error (no retry logic) - Unexpected shutdown: Worker crashes → Operations rejected with
WorkerCrashedError(retry logic applies)
// Expected shutdown - no retry
await worker.close();
// Unexpected shutdown - retry logic applies
// (worker crashes due to OOM, SIGKILL, etc.)Complete Configuration Example
import { createWorker } from 'isolated-workers';
import type { UnexpectedShutdownConfig } from 'isolated-workers';
const shutdownConfig: UnexpectedShutdownConfig<MyMessages> = {
strategy: 'reject', // Default: reject non-idempotent operations
// Idempotent read operations - safe to retry
fetchData: { strategy: 'retry', attempts: 3 },
validateSchema: { strategy: 'retry', attempts: 5 },
healthCheck: { strategy: 'retry', attempts: 5 },
// Idempotent writes with deduplication - can retry
upsertRecord: { strategy: 'retry', attempts: 2 },
// Non-idempotent writes - reject immediately
createInvoice: { strategy: 'reject' },
chargePayment: { strategy: 'reject' },
publishEvent: { strategy: 'reject' },
};
const worker = await createWorker<MyMessages>({
script: './worker.js',
unexpectedShutdown: shutdownConfig,
timeout: 60000, // Configure message timeout separately
});
// Usage with error handling
async function processDataSafely(data: unknown) {
try {
return await worker.send('fetchData', { id: data.id });
} catch (error) {
if (error instanceof WorkerCrashedError) {
console.error(
`Worker crashed on attempt ${error.attempt}/${error.maxAttempts}`
);
// Check if we should escalate
if (error.reason.type === 'exit' && error.reason.signal === 'SIGKILL') {
alert('Worker was forcibly killed - check system resources');
}
throw error; // Re-throw or implement fallback
}
throw error;
}
}API Reference
WorkerOptions.unexpectedShutdown
Optional configuration for handling unexpected worker shutdowns.
Type: UnexpectedShutdownConfig
Default: { strategy: 'reject' }
UnexpectedShutdownStrategy
Strategy for handling operations when a worker crashes.
type UnexpectedShutdownStrategy =
| { strategy: 'reject' } // Reject pending operations immediately
| { strategy: 'retry'; attempts?: number }; // Retry operations (default: 1 attempt)UnexpectedShutdownConfig
Worker-level shutdown strategy with optional per-message-type overrides.
type UnexpectedShutdownConfig<T extends MessageDefinitions> =
UnexpectedShutdownStrategy & {
[K in MessageType<T>]?: UnexpectedShutdownStrategy;
};WorkerCrashedError
Error thrown when a worker crashes during operation processing.
class WorkerCrashedError extends Error {
readonly reason: ShutdownReason; // Crash details
readonly messageType: string; // Message type being processed
readonly attempt: number; // Current attempt number
readonly maxAttempts: number; // Maximum configured attempts
}ShutdownReason
Details about why a worker shut down.
type ShutdownReason =
| { type: 'exit'; code: number | null; signal: string | null }
| { type: 'error'; error: Error }
| { type: 'close' };See Also
- Error Handling - Error propagation patterns
- Timeout Configuration - Configure operation timeouts
- Worker Lifecycle - Managing worker state and cleanup