The Node.js event loop got stuck because a promise rejected, and no one was listening.
This usually happens when you have asynchronous operations that might fail, and you don’t have a .catch() block or a try...catch around your await to handle those potential failures. Node.js, by default, tries to be helpful and warns you when a promise rejects without a handler, because unhandled rejections can lead to unexpected application behavior or even crashes.
Here are the most common reasons this bites you:
1. Missing .catch() on a Promise Chain:
This is the classic. You’ve got a chain of .then() calls, and one of them throws an error or returns a rejected promise, but the chain ends without a .catch().
- Diagnosis: Look for promise chains that end abruptly. Search your codebase for
.then(without a subsequent.catch(. - Fix: Add a
.catch()at the end of the chain.
This works because thesomeAsyncOperation() .then(result => anotherAsyncOperation(result)) .then(finalResult => console.log(finalResult)) .catch(error => console.error('Something went wrong:', error)); // <-- This is the fix.catch()block acts as a universal handler for any rejection that occurs anywhere in the preceding promise chain.
2. Unhandled Rejection in async/await without try...catch:
When you use async/await, it’s easy to forget that await will throw an error if the promise it’s waiting on rejects. Without a try...catch block, that error becomes an unhandled rejection.
- Diagnosis: Scan
asyncfunctions forawaitexpressions that aren’t wrapped intry...catch. - Fix: Wrap the
awaitcalls in atry...catchblock.
Theasync function doSomethingRisky() { try { const result = await potentiallyFailingOperation(); console.log(result); } catch (error) { // <-- This is the fix console.error('Operation failed:', error); } }try...catchblock intercepts any error thrown byawait(which is what happens when a promise rejects) and allows you to handle it gracefully.
3. Ignoring Promise Return Values:
Sometimes, you might call a function that returns a promise but don’t do anything with that promise – no .then(), no .catch(), and not awaiting it. If that returned promise rejects, it’s unhandled.
- Diagnosis: Look for function calls that are known to return promises, but the return value isn’t used.
- Fix: Either assign the promise to a variable and attach a
.catch(), orawaitit.
By actively managing the promise (either through chaining or// Bad: // readFileAsync('config.json'); // Good (using .catch): readFileAsync('config.json').catch(err => console.error('Failed to read config:', err)); // Good (using async/await): async function loadConfig() { try { const config = await readFileAsync('config.json'); // ... use config } catch (err) { console.error('Failed to read config:', err); } }await), you ensure that its eventual outcome, success or failure, is accounted for.
4. Rejections from Event Emitters (e.g., EventEmitter.once):
If you’re using event emitters and a listener attached via once or on throws an error or returns a rejected promise, it can sometimes lead to unhandled rejections if not properly managed. While less common for direct promise rejections, the underlying principles apply.
- Diagnosis: Examine how you’re attaching listeners to
EventEmitters, especially if those listeners involve asynchronous operations. - Fix: Ensure any asynchronous logic within event listeners is also wrapped in error handling.
This prevents errors originating from within the event handler itself from bubbling up as unhandled exceptions.const myEmitter = new EventEmitter(); myEmitter.once('data', async () => { try { await processData(); } catch (error) { // <-- Error handling within the listener console.error('Error processing data:', error); } });
5. Third-Party Libraries Not Handling Their Own Rejections: You might be using a library that returns promises, but the library itself might have a bug where it doesn’t handle rejections internally, leading to unhandled rejection warnings from your application’s perspective.
- Diagnosis: When the warning points to code within a
node_modulesdirectory, investigate the library’s API. Check its documentation for error handling patterns. - Fix: Wrap calls to the problematic library functions with your own
.catch()ortry...catch.
This creates a defensive layer around external code, ensuring that even if the library fails to catch its own errors, your application doesn’t crash.const library = require('some-unreliable-library'); async function useLibrary() { try { const result = await library.doSomething(); console.log(result); } catch (error) { // <-- Your safeguard console.error('Error from external library:', error); } }
6. Implicit Rejections in Synchronous Code: While less direct, sometimes synchronous code that should throw an error doesn’t, and instead returns an unexpected value that a subsequent promise chain then tries to process, leading to a rejection downstream.
- Diagnosis: Trace the data flow backward from the point of rejection. Look for synchronous functions that might return invalid states or
undefinedwhen they should throw. - Fix: Add explicit error throwing in synchronous code where appropriate.
By making synchronous code more robust with explicit error throwing, you prevent invalid states from propagating into asynchronous operations and causing unexpected rejections.function validateInput(data) { if (!data || typeof data.value === 'undefined') { throw new Error('Invalid input: missing value'); // <-- Explicit throw } return data; } async function processValidatedData() { const input = getInput(); // This might return invalid data const validated = validateInput(input); // If validateInput doesn't throw, validated might be bad const result = await process(validated); console.log(result); }
Once all these are fixed, the next error you’ll likely encounter is ERR_QUIC_SOCKET_CLOSED if you’re using QUIC/HTTP3 and the underlying connection is abruptly terminated due to a previous, unhandled network issue.