Introduction
Since Node.js v15, unhandled promise rejections cause the process to exit with a non-zero code. Previously, they only emitted a warning. Any async function that rejects without a .catch() handler, or any await that throws without a try/catch, will crash the entire process. This makes proper error handling in async code critical for production stability.
Symptoms
node:internal/process/promises:288 triggerUncaughtException(err, true);UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block- Process exits with code 1 after async error
[UnhandledPromiseRejection: Error: connect ETIMEDOUT]- Server crashes on a single failed API call
``` node:internal/process/promises:288 triggerUncaughtException(err, true /* fromPromise */); ^
[UnhandledPromiseRejection: Error: connect ETIMEDOUT 10.0.0.1:443] code: 'ETIMEDOUT', errno: -110, syscall: 'connect', address: '10.0.0.1', port: 443 ]
Node.js v20.11.0 ```
Common Causes
- Async function without try/catch around await
- Promise chain without
.catch()at the end - Fire-and-forget async calls:
asyncOperation()without await or catch - Error thrown inside
.then()without downstream.catch() - EventEmitter async handlers that reject
Step-by-Step Fix
- 1.Add global unhandled rejection handler:
- 2.```javascript
- 3.// Prevent crash and log the error
- 4.process.on('unhandledRejection', (reason, promise) => {
- 5.console.error('Unhandled Rejection at:', promise, 'reason:', reason);
- 6.// In production, send to error tracking service
- 7.// Sentry.captureException(reason);
- 8.});
// NOTE: This prevents the crash but does not fix the underlying bug // Always fix the root cause - do not rely on this handler long-term ```
- 1.Use try/catch with async/await:
- 2.```javascript
- 3.// WRONG - rejection not caught
- 4.async function fetchData() {
- 5.const response = await fetch('https://api.example.com/data');
- 6.const data = await response.json(); // Throws if response is not JSON
- 7.return data;
- 8.}
- 9.fetchData(); // Unhandled rejection if fetch fails
// CORRECT - wrap in try/catch
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(HTTP ${response.status}: ${response.statusText});
}
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to fetch data:', error.message);
throw error; // Re-throw if caller should handle it
}
}
// Caller must also handle try { const data = await fetchData(); } catch (error) { // Handle or log } ```
- 1.Use .catch() for fire-and-forget operations:
- 2.```javascript
- 3.// WRONG - no error handling
- 4.async function sendAnalytics(event) {
- 5.await fetch('https://analytics.example.com/track', {
- 6.method: 'POST',
- 7.body: JSON.stringify(event),
- 8.});
- 9.}
- 10.sendAnalytics({ page: '/home' }); // Unhandled rejection if fetch fails
// CORRECT - catch errors even for fire-and-forget function sendAnalytics(event) { fetch('https://analytics.example.com/track', { method: 'POST', body: JSON.stringify(event), }).catch((err) => { // Log but don't crash - analytics failure is not critical console.error('Analytics send failed:', err.message); }); } ```
- 1.Handle errors in Express async route handlers:
- 2.```javascript
- 3.// WRONG - async errors not caught by Express
- 4.app.get('/users/:id', async (req, res) => {
- 5.const user = await User.findById(req.params.id);
- 6.res.json(user); // Error if user not found or DB fails
- 7.});
// CORRECT - wrap in try/catch or use async handler wrapper const asyncHandler = (fn) => (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); };
app.get('/users/:id', asyncHandler(async (req, res) => { const user = await User.findById(req.params.id); if (!user) { return res.status(404).json({ error: 'User not found' }); } res.json(user); })); ```
- 1.Debug which promise is unhandled:
- 2.```bash
- 3.# Run with detailed rejection tracking
- 4.node --trace-uncaught app.js
# Or set environment variable NODE_OPTIONS="--trace-uncaught" npm start
# This shows the full promise chain and where the rejection originated ```
Prevention
- Always use try/catch with async/await or .catch() on promise chains
- Use
express-async-errorsorexpress-async-handlerfor Express routes - Add
process.on('unhandledRejection')as a safety net (not a fix) - Use ESLint rule
no-floating-promisesfrom@typescript-eslint - Write tests that verify error paths in async functions
- Use
await-to-jspattern for cleaner error handling: - ```javascript
- const to = require('await-to-js').default;
- const [err, data] = await to(fetchData());
- if (err) { /* handle error */ }
`