Node.js 비동기 에러 핸들링 - Promise 체이닝 vs try-catch
문제 상황
외부 API를 호출하는 Node.js 서버를 운영 중이었는데, 간헐적으로 서버가 다운되는 현상이 발생했다. 로그를 확인해보니 UnhandledPromiseRejectionWarning이 원인이었다.
app.get('/api/user/:id', (req, res) => {
fetchUserFromDB(req.params.id)
.then(user => {
return callExternalAPI(user.token); // 여기서 에러 발생
})
.then(data => res.json(data));
});
마지막 .catch()가 없어서 Promise rejection이 처리되지 않았다.
Promise 체이닝 방식
가장 기본적인 해결법은 .catch() 추가다.
app.get('/api/user/:id', (req, res) => {
fetchUserFromDB(req.params.id)
.then(user => callExternalAPI(user.token))
.then(data => res.json(data))
.catch(err => {
console.error(err);
res.status(500).json({ error: 'Internal Server Error' });
});
});
async/await + try-catch
Node 8부터 async/await를 본격적으로 사용할 수 있게 되면서, 더 직관적인 에러 핸들링이 가능해졌다.
app.get('/api/user/:id', async (req, res) => {
try {
const user = await fetchUserFromDB(req.params.id);
const data = await callExternalAPI(user.token);
res.json(data);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Internal Server Error' });
}
});
글로벌 에러 핸들러
개별 라우트마다 try-catch를 작성하는 것도 번거롭고, 놓치는 경우가 생긴다. Express 미들웨어로 전역 핸들러를 만들었다.
const asyncHandler = fn => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
app.get('/api/user/:id', asyncHandler(async (req, res) => {
const user = await fetchUserFromDB(req.params.id);
const data = await callExternalAPI(user.token);
res.json(data);
}));
// 에러 핸들링 미들웨어
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Internal Server Error' });
});
결론
- Promise는 반드시
.catch()추가 - async/await 사용 시 try-catch 필수
- 래퍼 함수로 중복 제거 가능
process.on('unhandledRejection')로 최후의 안전장치 추가 고려
배포 후 일주일간 모니터링했는데 서버 크래시가 사라졌다.