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

이 글의 구성
🔎 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);
});
🛠 실무 전략과 전역 처리
패턴을 알았으면 실무에서 어떻게 조합하는지가 남는다. 몇 가지 원칙을 정리했다.
비동기 에러 처리 전략
마지막으로 전역 안전망이다. 아무리 조심해도 놓치는 거부가 생기니, 이를 한곳에서 로깅해 두면 조용한 실패를 막는다. 브라우저는 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 에러 처리 체크리스트
본 글은 자바스크립트 async/await 비동기 에러 처리의 일반적 패턴을 정리한 자료다. 코드 수정은 영향 범위를 확인한 뒤 신중히 적용한다.
#JavaScript #asyncawait #에러처리 #trycatch #Promise #PromiseallSettled #Promiseall #unhandledrejection #비동기 #finally #rethrow #fetch #프론트엔드 #JS디버깅 #웹개발
'IT' 카테고리의 다른 글
| let vs var 함정 완전 정리 — 스코프·호이스팅·반복문 클로저 (0) | 2026.07.02 |
|---|---|
| this가 undefined — 바인딩·화살표 함수 원인·해결 (0) | 2026.07.02 |
| Invalid array length — 배열 길이 오류 원인·해결·예방 (0) | 2026.07.02 |
| Cannot convert undefined or null to object — 원인·해결·예방 (0) | 2026.07.02 |
| Cannot access 'x' before initialization — TDZ 원인·해결·예방 (0) | 2026.07.02 |
댓글