Node.js 비동기 에러 핸들링 패턴 정리

문제 상황

새벽에 모니터링 알람이 울렸다. Node.js 서버가 갑자기 죽어있었다. 로그를 확인하니 uncaughtException이 원인이었다.

app.get('/api/users/:id', (req, res) => {
  db.query('SELECT * FROM users WHERE id = ?', [req.params.id], (err, result) => {
    if (err) throw err; // 여기서 던진 에러가 catch되지 않음
    res.json(result);
  });
});

콜백 내부에서 던진 에러는 try-catch로 잡을 수 없다는 걸 알면서도 습관적으로 throw를 사용했던 게 문제였다.

해결 방법

1. 콜백 패턴에서는 명시적 처리

app.get('/api/users/:id', (req, res) => {
  db.query('SELECT * FROM users WHERE id = ?', [req.params.id], (err, result) => {
    if (err) {
      console.error(err);
      return res.status(500).json({ error: 'Database error' });
    }
    res.json(result);
  });
});

2. Promise로 감싸기

function queryAsync(sql, params) {
  return new Promise((resolve, reject) => {
    db.query(sql, params, (err, result) => {
      if (err) reject(err);
      else resolve(result);
    });
  });
}

app.get('/api/users/:id', (req, res) => {
  queryAsync('SELECT * FROM users WHERE id = ?', [req.params.id])
    .then(result => res.json(result))
    .catch(err => {
      console.error(err);
      res.status(500).json({ error: 'Database error' });
    });
});

3. 전역 에러 핸들러

process.on('uncaughtException', (err) => {
  console.error('Uncaught Exception:', err);
  // 로깅 후 graceful shutdown
  process.exit(1);
});

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

결론

Node.js 8에서 async/await를 본격적으로 사용할 수 있게 되면서 에러 핸들링이 한결 나아질 것 같다. 지금은 Promise 패턴으로 통일하는 작업을 진행 중이다. 전역 핸들러는 최후의 안전망일 뿐, 각 레이어에서 적절히 처리하는 게 중요하다.