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 체인에 비해 코드 흐름이 직관적이고, 에러 처리도 일관성 있게 가져갈 수 있었다.