JavaScript Async Debugging Case Study | Tracing Errors in Promise Chains

JavaScript Async Debugging Case Study | Tracing Errors in Promise Chains

이 글의 핵심

Debugging async JavaScript: Promise chains, async/await, stack traces, and production monitoring.

Introduction

UnhandledPromiseRejection is one of the most common Node.js warnings. This post walks through finding and fixing errors in real async code.

What you will learn

  • Why Promise errors “disappear”
  • How to improve async stack traces
  • async/await error-handling patterns
  • Production monitoring (e.g. Sentry)

Table of contents

  1. Problem: intermittent unhandled rejections
  2. Symptom: errors vanish
  3. Tracing Promise chains
  4. Root cause: missing error handler
  5. Fix 1: async/await
  6. Fix 2: global handlers
  7. Fix 3: error boundary pattern
  8. Monitoring: Sentry
  9. Closing thoughts

1. Problem

Logs

(node:12345) UnhandledPromiseRejectionWarning: Error: Database connection failed
    at Database.connect (database.js:45:15)

Traits

  • Hard to repro locally
  • A few times per day
  • Little context on caller

2. Symptom

Buggy route

app.get('/users/:id', (req, res) => {
  getUserData(req.params.id)
    .then(user => {
      res.json(user);
    });
  // Missing .catch()
});

async function getUserData(id) {
  const user = await db.query('SELECT * FROM users WHERE id = ?', id);
  
  if (!user) {
    throw new Error('User not found');
  }
  
  return user;
}

Why it fails silently at the edge

Rejection propagates; without .catch (or try/catch in an async handler), Node reports UnhandledPromiseRejection.


3. Tracing

{
  "scripts": {
    "start": "node --trace-warnings --async-stack-traces server.js"
  }
}

Improved stacks often show the route file and line.


4. Root cause

Common patterns:

  • .then without .catch
  • async handler without try/catch and no Express error forward
  • .catch that only logs and swallows errors needed downstream

5. Fix 1: async/await

app.get('/users/:id', async (req, res) => {
  try {
    const user = await getUserData(req.params.id);
    res.json(user);
  } catch (err) {
    console.error('Error fetching user:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

6. Fix 2: global handlers

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
  errorLogger.log({
    type: 'unhandledRejection',
    reason: reason,
    stack: reason.stack,
    timestamp: new Date().toISOString(),
  });
});

process.on('uncaughtException', (err) => {
  console.error('Uncaught Exception:', err);
  process.exit(1);
});

Express wrapper

function asyncHandler(fn) {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await getUserData(req.params.id);
  res.json(user);
}));

app.use((err, req, res, next) => {
  console.error('Error:', err);
  res.status(500).json({ error: err.message });
});

7. Fix 3: service layer

Map infrastructure errors to domain errors (NotFoundError, ServiceUnavailableError) and handle by type in one error middleware.


8. Monitoring

const Sentry = require('@sentry/node');

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 1.0,
});

app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());
app.use(Sentry.Handlers.errorHandler());

9. Patterns

  • Promise.allSettled vs bare Promise.all when partial failure is OK
  • Timeouts with Promise.race
  • Retries with exponential backoff

Closing thoughts

  1. Every async path needs a rejection handler (.catch or try/catch or asyncHandler)
  2. Global safety net for slips
  3. Sentry (or similar) for production
  4. async/await for readability

Assume errors will happen—design the path explicitly.


FAQ

Q1. Promise chains vs async/await?

Prefer async/await with try/catch at boundaries; chains are fine with explicit .catch.

Q2. try/catch everywhere?

Centralize at HTTP layer + domain boundaries; don’t wrap every line.

Q3. Should unhandled rejections kill the process?

Modern Node may exit; use process managers and fix root causes.


  • JavaScript async programming
  • JavaScript Promises
  • JavaScript async/await

Keywords

JavaScript, async, Promise, async/await, Unhandled Rejection, error handling, debugging, Sentry, Node.js, case study