Express 미들웨어에서 비동기 에러 핸들링 개선하기

문제 상황

프로젝트에서 Express 라우트 핸들러를 async/await로 전환하면서 모든 핸들러마다 try-catch를 작성해야 했다. 비동기 함수에서 발생한 에러는 자동으로 Express 에러 핸들러로 전달되지 않기 때문이다.

app.get('/users/:id', async (req, res) => {
  try {
    const user = await User.findById(req.params.id);
    res.json(user);
  } catch (err) {
    next(err); // next를 파라미터로 추가해야 함
  }
});

매번 이런 보일러플레이트를 작성하는 게 번거로웠다.

해결 방법

비동기 핸들러를 감싸는 래퍼 함수를 만들었다.

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

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

프로미스 체인에서 발생한 에러를 catch해서 next로 전달한다. 이제 try-catch 없이도 에러가 글로벌 에러 핸들러로 전달된다.

전역 에러 핸들러

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({
    error: {
      message: err.message,
      ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
    }
  });
});

개발 환경에서는 스택 트레이스를 포함하고, 프로덕션에서는 민감한 정보를 숨긴다.

결과

코드가 간결해졌고 에러 처리 로직이 일관성 있게 유지됐다. express-async-errors 같은 라이브러리도 있지만, 직접 구현한 래퍼가 더 명시적이고 제어 가능해서 만족스러웠다.