Node.js 비동기 에러 핸들링 삽질기

문제 상황

배포 후 간헐적으로 Node 프로세스가 조용히 종료되는 현상이 발생했다. 로그를 뒤져보니 Promise rejection이 제대로 처리되지 않은 것이 원인이었다.

app.get('/api/users/:id', (req, res) => {
  getUserFromDB(req.params.id)
    .then(user => res.json(user));
  // catch가 없음!
});

임시 방편

일단 프로세스 레벨에서 unhandled rejection을 잡도록 했다.

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
  // 로깅 후 프로세스 종료
  process.exit(1);
});

근본적인 해결

모든 비동기 라우트 핸들러에 에러 처리를 추가했다. Express는 기본적으로 Promise rejection을 잡지 못하기 때문에 래퍼 함수를 만들었다.

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

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

그리고 전역 에러 핸들러를 추가했다.

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

교훈

Node 8에서 Promise를 쓸 때는 항상 rejection 처리를 명시적으로 해야 한다. async/await이 정착되면 좀 나아지겠지만, 현재로선 래퍼 패턴이 가장 실용적이다.