본문 바로가기
IT

async/await 에러 처리 완전 정리 — try-catch·allSettled·전역 처리

by 샤나엘 2026. 7. 2.
반응형

async/await 에러 처리 완전 정리 — try-catch·allSettled·전역 처리

비동기 코드는 잘 돌 때는 깔끔하지만, 에러 처리를 빼먹으면 조용히 무너진다. fetch가 실패해도, 서버가 이상한 값을 줘도, 처리를 안 해 두면 앱은 멀쩡한 척하다가 엉뚱한 곳에서 터진다. async/await는 비동기 코드를 동기처럼 읽히게 해 주는데, 에러 처리도 동기 코드의 try-catch를 그대로 쓸 수 있어 직관적이다. 핵심 원리는 하나다. await한 Promise가 거부(reject)되면 그 자리에서 예외가 던져지고, 그걸 try-catch로 잡는다. 이 글은 async/await 에러 처리의 기본, 흔히 놓치는 실수, 그리고 Promise.all과 allSettled·전역 처리까지 실무 패턴을 정리한다.

async/await 에러 처리 완전 정리

 

이 글의 구성

 

🔎async/await 에러 처리의 기본
🧩흔히 놓치는 실수 4가지
💻올바른 패턴 — try-catch·finally
⚖️Promise.all vs allSettled
🛠실무 전략과 전역 처리
💬자주 묻는 질문 5가지

🔎 async/await 에러 처리의 기본

async 함수 안에서 await는 Promise가 끝나기를 기다린다. 그 Promise가 성공(resolve)하면 값을 돌려주고, 실패(reject)하면 그 자리에서 예외를 던진다. 바로 이 점 덕분에 동기 코드처럼 try-catch로 감쌀 수 있다. try 블록 안에서 await한 것이 거부되면, catch가 그 에러를 받는다.

// ✓ 기본형 — try 안에서 await, catch로 처리
async function load() {
  try {
    const res = await fetch("/api/data");
    const data = await res.json();
    return data;
  } catch (e) {
    console.error("불러오기 실패:", e.message);
  }
}

핵심은 "await가 거부를 예외로 바꿔 준다"는 것이다. 그래서 비동기 에러도 동기 에러와 똑같은 방식으로 잡을 수 있다. 다만 이 편리함에는 조건이 있다. await가 반드시 try 블록 안에 있어야 하고, Promise를 만드는 호출에 await를 빠뜨리면 안 된다. 이 조건을 어기는 게 대부분의 실수다.

핵심 — 거부는 예외로 던져진다

await한 Promise가 거부되면 그 줄에서 예외가 던져지고, 감싼 try-catch가 잡는다. 그래서 비동기 에러 처리가 동기처럼 직관적이다. 단, await가 try 안에 있고 호출에 await를 붙였을 때만 성립한다. 이 둘을 어기면 에러가 catch를 빠져나간다.


🧩 흔히 놓치는 실수 4가지

실수 1 — await를 빠뜨림

가장 흔하다. try 블록 안에서 async 함수를 호출하며 await를 안 붙이면, 그 Promise는 try-catch 밖에서 따로 흘러가 거부돼도 catch에 안 잡힌다. 처리 안 된 거부(unhandled rejection)로 남는다.

실수 2 — await가 try 밖에 있음

await를 try 블록 바깥에서 실행하면, 거기서 나는 거부는 그 try-catch가 못 잡는다. 잡고 싶은 비동기 작업은 반드시 try 블록 안에서 await해야 한다.

실수 3 — 에러를 잡고 그냥 삼킴

catch에서 아무 처리도 안 하거나 로그만 찍고 넘기면, 호출한 쪽은 성공한 줄 안다. 에러를 잡았으면 사용자에게 알리거나, 필요하면 다시 던져(rethrow) 상위에서 처리하게 해야 한다.

실수 4 — 여러 작업을 순차로만 처리

독립적인 여러 비동기 작업을 await로 하나씩 기다리면 느리고, 그중 하나가 실패했을 때의 처리도 애매해진다. 병렬로 묶되 실패를 어떻게 다룰지(Promise.all·allSettled)를 정해야 한다.


💻 올바른 패턴 — try-catch·finally

기본 try-catch에 더해, 정리 코드는 finally에 두고, 필요하면 에러를 다시 던져 상위로 전파한다. 이 세 가지를 익히면 대부분의 상황을 덮는다.

// ✓ finally로 정리, 필요 시 rethrow
async function load() {
  setLoading(true);
  try {
    const res = await fetch("/api/data");
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (e) {
    console.error(e);
    throw e; // 상위에서도 처리하게 다시 던짐
  } finally {
    setLoading(false); // 성공·실패 무관하게 실행
  }
}

.then 체인을 쓰는 코드라면 try-catch 대신 .catch를 끝에 붙인다. 두 방식은 목적이 같다. 거부가 흘러갈 종착지를 만드는 것이다.

// ✓ Promise 체인은 .catch 로 (finally 도 가능)
fetch("/api/data")
  .then(res => res.json())
  .then(data => render(data))
  .catch(err => console.error(err))
  .finally(() => setLoading(false));

⚖️ Promise.all vs allSettled

독립적인 여러 비동기 작업을 병렬로 처리할 때, 실패를 어떻게 다룰지에 따라 도구가 갈린다. 이 선택이 에러 처리의 핵심이다.

메서드 하나가 실패하면 언제 쓰나
Promise.all 즉시 전체 거부(나머지 결과 버림) 전부 성공해야 의미 있을 때
Promise.allSettled 거부 안 함(각 성공·실패 배열 반환) 일부 실패해도 나머지 쓸 때
Promise.any 하나라도 성공하면 그 값 가장 빨리 성공한 하나만 필요할 때

Promise.all은 하나라도 거부되면 즉시 전체가 거부돼, 나머지 성공 결과까지 버린다. 다만 이미 시작된 다른 작업이 멈추는 건 아니다. JS Promise는 취소가 안 되므로 나머지도 끝까지 돌고, 그 결과만 무시될 뿐이다. 그래서 Promise.all은 "전부 성공해야만 진행"할 때 알맞다. 반면 Promise.allSettled는 어느 것도 거부하지 않고, 각 작업의 상태(fulfilled·rejected)와 값·사유를 담은 배열을 돌려준다. 일부가 실패해도 성공한 것만 골라 쓰고 싶을 때 이걸 쓴다.

// ✓ 일부 실패 허용 — allSettled
const results = await Promise.allSettled([getA(), getB(), getC()]);
results.forEach(r => {
  if (r.status === "fulfilled") use(r.value);
  else console.warn("실패:", r.reason);
});

🛠 실무 전략과 전역 처리

패턴을 알았으면 실무에서 어떻게 조합하는지가 남는다. 몇 가지 원칙을 정리했다.

비동기 에러 처리 전략

01거부가 날 자리마다 try-catch·.catch로 종착지를 만든다.
02독립 작업은 병렬로(Promise.all·allSettled), 의존 작업만 순차 await.
03잡은 에러는 사용자 알림·복구로 이어지게, 필요하면 rethrow로 전파.
04fetch는 res.ok를 확인해 직접 throw(4xx·5xx는 거부 안 함).
05전역 안전망으로 놓친 거부를 로깅(개별 처리를 대체하진 않음).

마지막으로 전역 안전망이다. 아무리 조심해도 놓치는 거부가 생기니, 이를 한곳에서 로깅해 두면 조용한 실패를 막는다. 브라우저는 unhandledrejection 이벤트로 처리 안 된 모든 거부를 잡을 수 있다. 단, 이건 어디까지나 최후의 그물이지, 각 자리의 try-catch를 대신하지 않는다.

// ✓ 전역 안전망 (로깅용)
window.addEventListener("unhandledrejection", (e) => {
  console.error("처리 안 된 거부:", e.reason);
});

💬 자주 묻는 질문 5가지

Q1try-catch를 했는데 에러가 안 잡혀요.

await 없이 호출했거나, await가 try 블록 밖에 있을 가능성이 큽니다. await를 빠뜨리면 그 Promise는 try-catch와 별개로 흘러가 거부돼도 안 잡힙니다. 잡으려는 비동기 작업은 try 블록 안에서 await하세요.

Q2try-catch랑 .catch 중 뭘 쓰나요?

async/await 코드에는 try-catch가, .then 체인에는 .catch가 자연스럽습니다. 둘 다 거부를 받는 종착지를 만드는 같은 목적이죠. 한 함수 안에서는 한 방식으로 일관되게 쓰는 게 읽기 좋습니다.

Q3Promise.all이 자꾸 전체 실패해요.

Promise.all은 하나라도 거부되면 즉시 전체가 거부되는 구조입니다. 일부 실패를 견디고 나머지를 쓰고 싶다면 Promise.allSettled로 바꾸세요. 각 작업의 성공·실패를 배열로 받아 개별 처리할 수 있습니다.

Q4catch에서 다시 던지는(rethrow) 이유가 뭔가요?

여기서 로그만 남기고 상위에서도 처리해야 할 때 씁니다. catch에서 throw e를 하면 그 에러가 호출한 쪽으로 다시 전파돼, 상위의 try-catch가 이어받습니다. 로깅은 여기서, 사용자 안내는 상위에서 하는 식으로 역할을 나눌 수 있습니다.

Q5전역 unhandledrejection만 있으면 되나요?

최후의 안전망일 뿐입니다. 놓친 거부를 한곳에서 로깅해 조용한 실패를 막아 주지만, 실패한 요청 재시도나 사용자 알림 같은 구체적 처리는 못 합니다. 거부가 날 자리마다 try-catch를 두고, 전역 핸들러는 보조로 쓰세요.


📌 결론

async/await 에러 처리의 핵심은 하나다. await한 Promise가 거부되면 예외로 던져지고, 그걸 try-catch로 잡는다. 덕분에 비동기 에러도 동기처럼 다룰 수 있다. 다만 await를 빠뜨리거나 try 밖에서 await하면 에러가 새어 나가니, 잡을 작업은 try 안에서 await해야 한다.

 

실무 원칙은 다음과 같다.

🎣 거부를 반드시 잡는다. try 안에서 await하고, .then 체인은 .catch로 마무리한다. 정리는 finally, 전파는 rethrow로.

🔀 병렬과 실패 정책을 정한다. 독립 작업은 Promise.all·allSettled로 병렬 처리하되, 전부 성공이 필요하면 all, 일부 실패를 견디면 allSettled를 쓴다.

🌐 전역은 보조로 쓴다. fetch는 res.ok를 확인해 직접 throw하고, unhandledrejection 전역 핸들러는 놓친 거부를 로깅하는 안전망으로만 둔다.

async/await 에러 처리 체크리스트

01잡을 비동기 작업은 try 블록 안에서 await(await 누락 금지).
02.then 체인은 끝에 .catch, 정리는 finally.
03에러 삼키지 않기 — 알림·복구 또는 rethrow로 전파.
04병렬은 Promise.all(전부 성공)·allSettled(일부 실패 허용) 선택.
05fetch는 res.ok 확인 후 직접 throw.
06전역 unhandledrejection은 로깅 안전망으로만 보조.

본 글은 자바스크립트 async/await 비동기 에러 처리의 일반적 패턴을 정리한 자료다. 코드 수정은 영향 범위를 확인한 뒤 신중히 적용한다.

 

#JavaScript #asyncawait #에러처리 #trycatch #Promise #PromiseallSettled #Promiseall #unhandledrejection #비동기 #finally #rethrow #fetch #프론트엔드 #JS디버깅 #웹개발

반응형

댓글