DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Building a Voice-Powered Smart Kitchen App Using LLaMA 3.1, Firebase, and Node.js
  • The Best Node.js IDEs in the Market Right Now
  • How Good Is Node.js for Backend Development?
  • Do's and Don'ts for Making Your NodeJS Application Faster

Trending

  • Liquid Glass, Material 3, and a Lot of Plumbing
  • Is the Data Warehouse Dead? 3 Patterns From Enterprise Architecture That Answer This Question
  • Good Data, Bad Metric: A Mutation Testing Pattern for Analytics Engineering
  • Why Your Test Automation Is Always Behind the Code And the Architecture That Fixes It
  1. DZone
  2. Coding
  3. JavaScript
  4. Unhandled Promise Rejections: The Tiny Mistake That Crashed Our Node.js App

Unhandled Promise Rejections: The Tiny Mistake That Crashed Our Node.js App

A missed .catch() caused an unhandled promise rejection that crashed our Node.js API fix was proper async error handling with try... catch or middleware.

By 
Bhanu Sekhar Guttikonda user avatar
Bhanu Sekhar Guttikonda
DZone Core CORE ·
Oct. 24, 25 · Analysis
Likes (2)
Comment
Save
Tweet
Share
5.6K Views

Join the DZone community and get the full member experience.

Join For Free

Imagine deploying a Node.js backend service that works flawlessly in development, only to have it mysteriously crash in production. Everything ran fine on your laptop, but on the live server, the process keeps shutting down unexpectedly.

In our case, the culprit was a single unhandled promise rejection — one missing .catch() in our code caused Node to exit abruptly whenever an error occurred. That one “tiny” mistake made the difference between a stable service and frequent downtime. In this article, we’ll explore how a misconfigured error handling in a Node/Express API can bring down an application, and how to diagnose and fix it to prevent future crashes.

How Unhandled Rejections Appear in Node Applications

When a promise is rejected and nothing handles that error, Node.js will emit an unhandledRejection event. In modern Node versions (15+), this defaults to terminating the process with a fatal error. In older versions, the process would log a warning and continue running, which often led developers to ignore the issue, a dangerous practice.

In practical terms, an unhandled rejection can manifest as sudden application crashes or weird “silent” failures. If you’re running your Node app via a process manager or container, you might notice it restarting unexpectedly or logging an error right before exit. For example, Node might print a message like this just before dying:

SQL
 
(node:12345) UnhandledPromiseRejectionWarning: Unhandled promise rejection. 
This error originated either by throwing inside of an async function without a catch, 
or by rejecting a promise which was not handled with .catch(). 
(node:12345) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. 
In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.:contentReference[oaicite:2]{index=2}


On Node 16+ (where that “future” is now present), the process will terminate after such an error, often accompanied by a stack trace of the exception that was not caught. To clients or end-users, the service simply appears offline or unresponsive (e.g., API requests start failing or timing out) whenever this happens.

In short, an unhandled promise rejection in Node is like a ticking time bomb: the app might run fine until something goes wrong in an async function — then the browser or client sees failed requests (if it’s an API) and your server process crashes with little explanation. The trick is recognizing this scenario via the clues in logs and error messages.

Diagnosing an Unhandled Promise Rejection Crash

When your Node.js app is “randomly” crashing, a few clues can confirm an unhandled rejection issue:

Check the Server Logs or Console Output

Look for any mention of "UnhandledPromiseRejectionWarning" or an error stack with an uncaught exception. If you see Node complaints about an unhandled rejection (or a DeprecationWarning about them as shown above), that’s a strong sign. In Node 15+, you might not get a warning – instead, the process will exit with a non-zero code. Also, check your process manager’s restart logs or the system journal to see if the Node process died due to an uncaught error.

Reproduce With Debugging Flags

Run your app in a development/staging environment with strict promise rejection tracking. For example, launch Node with the --trace-warnings flag to get a full stack trace of where the unhandled promise was created. You can also use node --unhandled-rejections=strict app.js to force Node to crash on the first unhandled rejection (instead of just warning). This makes the problem surface immediately, helping you pinpoint the failing promise.

Use a Global Handler (Temporarily)

As a diagnostic step, you can attach a handler to the process.on('unhandledRejection') event to log out the rejection reason and stack. This will catch any unhandled promise and print what went wrong (without crashing). For example:

JavaScript
 
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
});


Using the above methods, you should be able to identify which operation or promise in your code is throwing an error without a catch. Once you know that, you can zero in on the offending code path.

Misconfigured Error Handling: A Code Example (What Not to Do)

Let’s consider a simplified example. Suppose we have an Express server with an endpoint that fetches data from a database:

JavaScript
 
// server.js (Node/Express backend)
app.get('/api/data', async (req, res) => {
  const data = await getImportantData();  // Potentially throws or rejects
  res.json(data);  // No error handling around the await
});


This looks straightforward — but it’s a trap. If getImportantData() fails (say the database query throws an exception or returns a rejected promise), the error will bubble up as an unhandled promise rejection because there’s no .catch() or try/catch around it. In an Express 4.x app, an uncaught error in an async route will not be caught by Express’s default error middleware (since Express isn’t aware of the rejected promise unless we catch and pass it).

What happens next? Node emits an unhandledRejection event, and since we didn’t handle it, Node considers it an uncaught fatal error. The server will log an error (or just terminate immediately in Node 16+), likely causing the process to crash. In our real-world scenario, this is exactly what happened: a database call deep in our code was rejected, and because a higher-level function forgot to handle it, the entire Node process went down.

Another common mistake is forgetting to await an async function. For example:

JavaScript
 
async function main() {
  getImportantData();  // forgot to await
  console.log("Done");
}
main();


Here, getImportantData() is called but not awaited. If it rejects, there’s no .catch() and the rejection is essentially “floating” in the background. This yields an unhandled rejection as well. A tiny oversight, like a missing await can thus be just as destructive as not enabling error handling at all.

In summary, any time we start an async operation without proper error handling, we risk a crash. Whether it’s a missing catch on a promise chain or a missing try...catch around an awaited call, these misconfigurations leave Node with no instructions on how to handle a failure, so the runtime’s policy is to fail hard (which is preferable to silently corrupting state).

Fixing It: Proper Error Handling (Express.js Example)

Once you’ve identified the unhandled promise causing the issue, the fix is straightforward: handle the error at the source, or ensure it’s passed to a handler that knows what to do. In a Node/Express context, you have a couple of options:

Try/Catch in Async Routes

The most direct fix for the code above is to wrap the await in a try...catch. This way, you catch the error and can respond or forward it properly instead of letting it kill the process. For example:

Here, if getImportantData() throws, our catch block passes the error to next(). Express will then send it to any error-handling middleware (like a function with signature (err, req, res, next) that sends a 500 response). The key is that the promise rejection is handled, so Node won’t consider it “unhandled” or crash. This pattern ensures the user gets an error response instead of a hung request, and our process stays alive.

Use a Promise .catch() on Pure Promise Chains

If you were using promises without async/await, make sure to tack on a .catch() at the end of the chain. For instance:

This accomplishes the same thing – any error is caught and handled (logging it and responding with an error status), rather than floating up to Node’s global handler.

JavaScript
 
const wrapAsync = fn => (req, res, next) => {
  fn(req, res, next).catch(next);
};
// Usage:
app.get('/api/data', wrapAsync(async (req, res) => {
  const data = await getImportantData();
  res.json(data);
}));


Leverage Middleware/Utilities

Manually adding try/catch in every route can be tedious and error-prone. In Express, you can use a wrapper or middleware to do this for you. One common approach is to create a higher-order function that wraps async route handlers and automatically catches any rejection, forwarding it to next(). For example:

Libraries like express-async-errors can also patch Express to handle promise rejections in route handlers, saving you from sprinkling try/catch everywhere. In our scenario, we added the necessary error handling around the offending code. Once we did, the Node process stopped crashing on that error – instead, it logged the error and returned a 500 response to the client, as intended. The frontend or API client might see an error message, but the service as a whole remained up, which is far better than a total outage. 

Also note: after fixing the immediate issue, consider if the error itself indicates a deeper bug (for example, a function that shouldn’t be throwing in the first place). Handling the error prevents the crash, but you should still investigate and resolve the root cause of the error to improve overall robustness.

Finally, what about that global process.on('unhandledRejection') handler we used while diagnosing? You can keep a version of it in production as a safety net if you use it wisely. For instance, you might want to log any unexpected unhandled rejections and maybe perform cleanup, then shut down the process gracefully. Example:

JavaScript
 
process.on('unhandledRejection', (reason, promise) => {
  console.error("Unhandled Rejection at:", promise, "reason:", reason);
  // Recommended: clean up and exit, to avoid unknown state
  process.exit(1);
});


This ensures that if you ever do miss a .catch elsewhere, you’ll know from the logs, and the process will exit (which, if you have a restarter, will bring it back fresh). However, do not rely on this as your primary error handling — it’s a last-resort failsafe. The Node documentation cautions that after an uncaught exception (or unhandled rejection treated as such), the process may be in an unstable state and should be terminated rather than resumed normally. So, use global handlers for logging and graceful shutdown, but focus on catching errors where they occur.

Best Practices to Prevent Unhandled Rejection Issues in Node.js

A few preventative measures can spare you from ever hitting an unhandled promise scenario:

  • Always handle your promises: Every promise chain should end with a .catch(), and every async/await call that could throw should be wrapped in a try/catch (or otherwise handled). As a rule, never let a promise go unhandled. If a function doesn’t need to handle an error itself, ensure it returns the promise to a caller that will handle it. This way, there's always someone catching the error down the line.
  • Use tools to catch omissions: Linters and type checkers can be a big help. For example, ESLint has rules like no-floating-promises that flag any promise that isn’t awaited or caught. Similarly, TypeScript can be configured to warn on unhandled promises. These tools act as a net, catching the “forgotten await” or missing .catch in code reviews or CI before it ever runs in production.
  • Wrap or abstract error handling in frameworks: If you’re using Express or a similar framework, adopt a pattern or library that handles async errors globally (e.g., the wrapAsync pattern or express-async-errors as shown above). This reduces the chance of a developer forgetting to catch an error in one of dozens of routes. Having a central error-handling middleware also ensures a consistent response to the client when things go wrong.
  • Fail fast in development: Don’t hide these issues during development. Always run your dev and staging servers with a Node version or setting that surfaces unhandled rejections. As mentioned, Node 15+ already crashes by default on them. If you’re on an older LTS version, use the --unhandled-rejections=strict flag to simulate that behavior. The goal is to catch the problem early, rather than having it silently lurking. Treat any warning about unhandled promises as critical and fix it on the spot.
  • Implement global handlers (carefully): As a safety net, consider adding process.on('unhandledRejection') and process.on('uncaughtException') handlers in your production app to log errors and perform last-minute cleanup. This can help you gather diagnostics if something slips through. Just remember: do not use these to simply suppress errors and continue. At minimum, log the details and then exit or restart the process to avoid running in a corrupted state. The best practice is still to fix the code, not rely on the global catch-all.
  • Monitor and test error scenarios: Use monitoring tools (APM, error tracking services like Sentry/New Relic) to catch unhandled rejections in the wild if they occur. And include failure scenarios in your testing: for example, simulate a database outage or an API call failure in a test environment to ensure your code properly handles the rejection instead of crashing. This kind of resilience testing can reveal missing catches before they hit production.

By following these practices, you’ll create a kind of “airbag” system for your Node.js application – one that catches errors gracefully instead of letting them bring down the whole service.

Conclusion

A small oversight in promise handling can have a huge impact — in our case, one uncaught promise was enough to crash an entire Node.js backend. The good news is that the fix was straightforward: by adding proper error handling (a few lines of code), we turned a flaky, crash-prone app into a robust one.

Unhandled promise rejections are often called the “silent killers” of Node apps, because they might not always scream in development logs but can wreak havoc in production. The key takeaway is to never leave promises unaccounted for. With careful coding (always include that .catch or try/catch), the help of tools and frameworks to enforce good practices, and vigilant testing, you can ensure that your Node.js applications handle errors gracefully instead of crumbling when something goes wrong. In short: handle your promises, and your Node app will handle itself under pressure.

Node.js app Crash (computing)

Opinions expressed by DZone contributors are their own.

Related

  • Building a Voice-Powered Smart Kitchen App Using LLaMA 3.1, Firebase, and Node.js
  • The Best Node.js IDEs in the Market Right Now
  • How Good Is Node.js for Backend Development?
  • Do's and Don'ts for Making Your NodeJS Application Faster

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook