Node.js 비동기 에러 핸들링 패턴 정리
문제 상황
새벽에 모니터링 알람이 울렸다. Node.js 서버가 갑자기 죽어있었다. 로그를 확인하니 uncaughtException이 원인이었다.
app.get('/api/users/:id', (req, res) => {
db.query('SELECT * FROM users WHERE id = ?', [req.params.id], (err, result) => {
if (err) throw err; // 여기서 던진 에러가 catch되지 않음
res.json(result);
});
});
콜백 내부에서 던진 에러는 try-catch로 잡을 수 없다는 걸 알면서도 습관적으로 throw를 사용했던 게 문제였다.
해결 방법
1. 콜백 패턴에서는 명시적 처리
app.get('/api/users/:id', (req, res) => {
db.query('SELECT * FROM users WHERE id = ?', [req.params.id], (err, result) => {
if (err) {
console.error(err);
return res.status(500).json({ error: 'Database error' });
}
res.json(result);
});
});
2. Promise로 감싸기
function queryAsync(sql, params) {
return new Promise((resolve, reject) => {
db.query(sql, params, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
}
app.get('/api/users/:id', (req, res) => {
queryAsync('SELECT * FROM users WHERE id = ?', [req.params.id])
.then(result => res.json(result))
.catch(err => {
console.error(err);
res.status(500).json({ error: 'Database error' });
});
});
3. 전역 에러 핸들러
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
// 로깅 후 graceful shutdown
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
});
결론
Node.js 8에서 async/await를 본격적으로 사용할 수 있게 되면서 에러 핸들링이 한결 나아질 것 같다. 지금은 Promise 패턴으로 통일하는 작업을 진행 중이다. 전역 핸들러는 최후의 안전망일 뿐, 각 레이어에서 적절히 처리하는 게 중요하다.