JavaScript 비동기 에러 핸들링 정리

문제 상황

API 호출 로직을 async/await으로 전환하는 과정에서 에러 핸들링이 누락된 부분들이 있었다. 특히 Promise chain에서 catch를 빼먹거나, async 함수 호출 시 await 없이 사용해 에러가 제대로 전파되지 않는 경우가 많았다.

// 문제가 있던 코드
async function fetchUserData(userId) {
  const response = await fetch(`/api/users/${userId}`);
  return response.json(); // 에러 처리 없음
}

// 호출부에서도 에러 처리 없음
fetchUserData(123); // unhandled rejection

해결 방법

1. try-catch 일관되게 사용

async function fetchUserData(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error('Failed to fetch user:', error);
    throw error; // 상위로 전파
  }
}

2. 전역 에러 핸들러 등록

window.addEventListener('unhandledrejection', event => {
  console.error('Unhandled promise rejection:', event.reason);
  // 에러 로깅 서비스로 전송
});

3. Wrapper 함수 활용

반복되는 try-catch를 줄이기 위해 wrapper 함수를 만들었다.

const asyncHandler = fn => {
  return async (...args) => {
    try {
      return await fn(...args);
    } catch (error) {
      // 공통 에러 처리
      logError(error);
      throw error;
    }
  };
};

const fetchUserData = asyncHandler(async (userId) => {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
});

정리

  • async 함수는 항상 Promise를 반환하므로 호출부에서도 에러 처리 필요
  • fire-and-forget 패턴이 필요한 경우가 아니라면 await 사용
  • 전역 핸들러는 있지만 로컬 에러 처리가 우선
  • TypeScript 도입을 고려 중인데, 타입 체크로 일부 실수를 방지할 수 있을 것 같다