async/await 도입 후 에러 핸들링 정리
배경
Node 8로 업그레이드하면서 async/await를 프로덕션에 적용하기 시작했다. 기존 Promise 체인 방식보다 코드가 확실히 깔끔해졌지만, 팀 내에서 에러 핸들링 방식이 제각각이었다.
기존 방식의 문제
async function fetchUser(id) {
const user = await User.findById(id);
const orders = await Order.findByUserId(user.id);
return { user, orders };
}
위 코드는 에러가 발생하면 상위로 전파되는데, 호출하는 쪽에서 매번 try-catch를 써야 했다.
적용한 패턴
1. Controller 레벨에서 통합 처리
const asyncHandler = fn => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
router.get('/users/:id', asyncHandler(async (req, res) => {
const user = await fetchUser(req.params.id);
res.json(user);
}));
2. 명시적 에러 핸들링이 필요한 경우
async function fetchUser(id) {
try {
const user = await User.findById(id);
if (!user) throw new NotFoundError('User not found');
return user;
} catch (error) {
logger.error('fetchUser failed', { id, error });
throw error;
}
}
3. 부분 실패 허용
async function getUserWithOptionalData(id) {
const user = await User.findById(id);
let recommendations = [];
try {
recommendations = await getRecommendations(user.id);
} catch (error) {
logger.warn('Failed to fetch recommendations', error);
}
return { user, recommendations };
}
결론
전역 에러 핸들러와 asyncHandler로 기본 처리를 하고, 비즈니스 로직상 필요한 경우만 명시적으로 try-catch를 쓰는 방향으로 컨벤션을 정했다. Promise 체인에 비해 코드 흐름이 직관적이고, 에러 처리도 일관성 있게 가져갈 수 있었다.