본문 바로가기
Q&A해결2026년 7월 12일2분 읽기

Express에서 async 라우트 안에서 에러 던지면 서버가 그냥 죽어버립니다

e
express_dohyun
조회 1021댓글 2

Express 4로 API 서버 돌리고 있는데요, 라우트 핸들러를 async 함수로 쓰고 그 안에서 await 하는 DB 호출이 reject 되면 프로세스가 통째로 죽습니다. 에러 핸들링 미들웨어(인자 4개짜리)는 분명히 등록해놨는데 거기로 안 들어와요.

동기 코드에서 throw 하면 잘 잡히는데, async 안에서 throw 하거나 await한 promise가 reject되면 unhandledRejection 뜨면서 pm2가 재시작합니다.

app.get('/users/:id', async (req, res) => {
  const u = await db.findUser(req.params.id) // 여기서 reject되면
  res.json(u)
})

// 에러 미들웨어 (안 탐)
app.use((err, req, res, next) => {
  res.status(500).json({ error: err.message })
})

매번 try/catch로 감싸서 next(err) 부르면 되긴 하는데 라우트가 수십 개라 다 감싸는 게 너무 지저분합니다. 뭔가 깔끔한 방법이 없을까요?

댓글 2

김김영삼3개월 전
Express 4의 구조적 한계예요. Express 4 라우터는 핸들러가 리턴하는 Promise를 봐주지 않습니다. async 함수가 reject되면 그 rejection이 Express로 전달되지 않고 그냥 unhandledRejection으로 빠져나가는데, Node 15부터 unhandledRejection 기본 동작이 프로세스 종료라서 죽는 거예요. 동기 throw는 Express가 내부에서 try/catch로 감싸고 있어 잡히지만 async는 그 밖에서 터집니다.

해결은 결이 세 가지 있어요.

1) 제일 간단한 건 express-async-errors 패키지를 앱 진입점 맨 위에서 한 번 require 하는 겁니다. 라우터를 몽키패치해서 async 핸들러의 reject를 자동으로 next(err)로 넘겨줘요. 한 줄 추가로 끝나고 기존 라우트는 안 건드려도 됩니다.

2) 패키지 안 쓰고 싶으면 wrapper를 하나 만드세요. const wrap = fn => (req,res,next) => Promise.resolve(fn(req,res,next)).catch(next) 이런 걸 만들어서 app.get('/users/:id', wrap(async ...)) 식으로 감싸면 라우트마다 try/catch 안 써도 됩니다.

3) 근본은 Express 5로 올리는 거예요. Express 5부터는 라우터가 async 핸들러의 rejection을 자동으로 잡아서 에러 미들웨어로 보냅니다. 그래서 5에서는 위 코드가 그냥 동작해요.

당장은 1번 넣어서 급한 불 끄고, 여유 될 때 Express 5 마이그레이션을 검토하시는 걸 추천합니다. 그리고 어느 방법을 쓰든 process.on('unhandledRejection', ...)에 로깅 하나는 걸어두세요. wrapper가 놓친 게 있으면 거기서 드러납니다.
eexpress_dohyun(guest)3개월 전
express-async-errors 한 줄로 끝났네요. try/catch 지옥에서 벗어났습니다 🙏 Express 5 마이그레이션도 정리되면 올려볼게요. 감사합니다.
Ctrl+Enter로 등록