Node.js 비동기 에러 핸들링과 Promise 체이닝
문제 상황
프로덕션 환경에서 간헐적으로 서버가 다운되는 이슈가 발생했다. 로그를 확인해보니 UnhandledPromiseRejectionWarning이 원인이었다.
app.get('/api/users/:id', (req, res) => {
db.query('SELECT * FROM users WHERE id = ?', [req.params.id])
.then(user => {
res.json(user);
});
});
Promise rejection이 catch되지 않으면 Node.js가 경고를 출력하고, 향후 버전에서는 프로세스가 종료될 예정이라고 한다.
해결 방법
1. 개별 라우트에 catch 추가
app.get('/api/users/:id', (req, res) => {
db.query('SELECT * FROM users WHERE id = ?', [req.params.id])
.then(user => {
res.json(user);
})
.catch(err => {
console.error(err);
res.status(500).json({ error: 'Internal Server Error' });
});
});
2. 에러 핸들링 래퍼 함수
매번 catch를 작성하는 것은 비효율적이어서 래퍼 함수를 만들었다.
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 db.query('SELECT * FROM users WHERE id = ?', [req.params.id]);
res.json(user);
}));
3. 글로벌 에러 핸들러
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
error: process.env.NODE_ENV === 'production'
? 'Internal Server Error'
: err.message
});
});
결론
Node.js 8에서 async/await를 본격적으로 사용하면서 에러 핸들링 패턴을 확립하는 것이 중요했다. 래퍼 함수 패턴은 코드 중복을 줄이고 일관된 에러 처리를 가능하게 했다. 향후 프로젝트에서는 이 패턴을 기본으로 적용할 예정이다.