Express 미들웨어 체이닝에서 에러 핸들링 처리

문제 상황

인증 미들웨어에서 DB 조회 중 에러가 발생했는데, 클라이언트는 계속 대기하다가 타임아웃이 발생했다. 로그를 확인해보니 에러가 발생했지만 Express의 에러 핸들러로 전달되지 않았다.

app.use(async (req, res, next) => {
  const user = await User.findById(req.session.userId);
  req.user = user;
  next();
});

미들웨어 내부의 Promise rejection이 catch되지 않아서 발생한 문제였다.

해결 방법

Express 4.x는 async/await을 네이티브로 지원하지 않는다. Promise가 reject되면 수동으로 next(error)를 호출해야 한다.

app.use(async (req, res, next) => {
  try {
    const user = await User.findById(req.session.userId);
    req.user = user;
    next();
  } catch (error) {
    next(error);
  }
});

매번 try-catch를 작성하는 게 번거로워서 래퍼 함수를 만들었다.

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

app.use(asyncHandler(async (req, res, next) => {
  const user = await User.findById(req.session.userId);
  req.user = user;
  next();
}));

에러 핸들러 정리

에러 핸들러는 4개의 파라미터를 받아야 Express가 인식한다.

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

이후 프로젝트의 모든 async 미들웨어에 asyncHandler를 적용했고, 더 이상 타임아웃 문제는 발생하지 않았다. Express 5에서는 Promise rejection을 자동으로 처리한다고 하니, 정식 출시되면 이런 보일러플레이트가 줄어들 것 같다.