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에 있다던데 정식 지원되면 좀 더 직관적으로 작성할 수 있을 것 같다.