ES6 Promise 체이닝에서 에러 처리 제대로 하기

문제 상황

사용자 정보를 조회한 후 권한을 확인하고 데이터를 가져오는 API 연쇄 호출 로직을 작성했는데, 중간에 에러가 발생해도 제대로 catch되지 않는 문제가 있었다.

fetchUser(userId)
  .then(user => {
    return checkPermission(user.id);
  })
  .then(permission => {
    return fetchData(permission.scope);
  })
  .catch(err => {
    console.error('에러 발생:', err);
  });

문제는 then 블록 내부에서 동기 에러가 발생했을 때였다. user 객체가 null인 경우 user.id에서 TypeError가 발생했지만, catch 블록으로 전달되지 않았다.

해결 방법

1. then 내부에서 발생한 에러는 다음 catch로 전달된다

fetchUser(userId)
  .then(user => {
    if (!user) {
      throw new Error('User not found');
    }
    return checkPermission(user.id);
  })
  .then(permission => {
    if (!permission) {
      throw new Error('Permission denied');
    }
    return fetchData(permission.scope);
  })
  .catch(err => {
    console.error('체인 내 에러:', err);
  });

2. catch 이후에도 체이닝 가능

fetchUser(userId)
  .then(user => checkPermission(user.id))
  .catch(err => {
    console.error('권한 확인 실패:', err);
    return { scope: 'default' }; // 기본값 반환
  })
  .then(permission => fetchData(permission.scope));

catch에서 값을 반환하면 다음 then으로 전달된다. 부분적 에러 복구가 필요할 때 유용했다.

3. finally 활용

let loading = true;

fetchUser(userId)
  .then(user => processUser(user))
  .catch(err => handleError(err))
  .finally(() => {
    loading = false;
  });

교훈

  • Promise 체인에서 throw한 에러는 다음 catch로 전달된다
  • catch도 then처럼 값을 반환할 수 있다
  • 항상 마지막에 catch를 붙여 Unhandled Rejection을 방지해야 한다

ES6 Promise를 쓰면서 콜백 지옥은 벗어났지만, 에러 처리 패턴은 여전히 신경 써야 할 부분이다.