ES6 Promise 체이닝 중 에러 핸들링 패턴 정리

문제 상황

사용자 인증 후 프로필 정보를 가져오는 로직에서 중간에 에러가 발생해도 후속 API가 계속 호출되는 버그가 있었다.

loginUser(credentials)
  .then(user => fetchProfile(user.id))
  .then(profile => updateUI(profile))
  .catch(err => console.error(err));

위 코드는 문제없어 보였지만, fetchProfile에서 401이 떨어져도 updateUI가 실행됐다.

원인

catch는 체인의 마지막에만 있어서, 중간 단계의 에러는 잡히지만 그 이후 로직은 멈추지 않는다고 생각했는데 착각이었다. 실제로는 catch 이전의 모든 reject가 전파되는 게 맞았다.

문제는 fetchProfile이 에러를 제대로 reject하지 않고 있었다.

function fetchProfile(userId) {
  return fetch(`/api/users/${userId}`)
    .then(res => res.json()); // 여기가 문제
}

HTTP 401이 떨어져도 fetch는 reject하지 않는다. Response 객체 자체는 정상적으로 반환되기 때문이다.

해결

function fetchProfile(userId) {
  return fetch(`/api/users/${userId}`)
    .then(res => {
      if (!res.ok) {
        throw new Error(`HTTP ${res.status}`);
      }
      return res.json();
    });
}

명시적으로 res.ok 체크를 추가해서 HTTP 에러를 reject로 변환했다. 이제 체인의 catch에서 제대로 잡힌다.

추가 패턴

에러 타입별로 다르게 처리해야 할 때는 커스텀 에러 클래스를 만들었다.

class AuthError extends Error {
  constructor(message) {
    super(message);
    this.name = 'AuthError';
  }
}

loginUser(credentials)
  .then(user => fetchProfile(user.id))
  .catch(err => {
    if (err instanceof AuthError) {
      redirectToLogin();
    } else {
      showErrorMessage(err.message);
    }
  });

Promise는 편하지만 에러 처리는 여전히 신경 써야 할 부분이 많다. async/await가 Stage 3에 있다던데 정식 지원되면 좀 더 직관적으로 작성할 수 있을 것 같다.