본문으로 건너뛰기

기다리는 일의 오류

자바스크립트 목차비동기레슨 62 / 76

목차

기다리는 일도 실패할 수 있습니다. 네트워크가 끊기거나 서버가 응답하지 않으면 Promise는 이행(fulfilled) 대신 거부(rejected) 상태가 됩니다. 이번 레슨에서는 async 함수 안에서 난 오류나 거부된 Promise를 try / catch로 안전하게 잡는 법과, 왜 await를 빼먹으면 try로 감싸도 오류가 잡히지 않는지 알아봅니다.

await와 try / catch로 오류 잡기

async 함수 안에서 throw new Error(...)를 실행하면, 그 함수가 돌려주는 Promise는 거부(rejected) 상태가 됩니다. 이 거부된 Promise 앞에 await를 붙이면 그 자리에서 다시 일반 오류처럼 던져지므로, 10단원에서 배운 try / catch로 그대로 잡을 수 있습니다.

async function load() {
  throw new Error('연결 끊김');
}
try {
  await load();
  console.log('성공');
} catch (e) {
  console.log(e.message);
}
실행 결과
연결 끊김

5번 줄의 await load()에서 거부된 Promise가 오류로 바뀌어 6번 줄을 건너뛰고 catch (e)로 넘어갑니다.

4단계 중 1단계. 6번 줄의 `return await load()`가 실행되면 `order()`는 `try` 블록 안에 머문 채 `load()`가 돌려준 Promise의 결과를 기다립니다. 2번 줄에서 던진 에러 때문에 `load()`의 Promise는 `거부(rejected)` 상태가 됩니다.

await를 빼먹으면 try가 잡지 못한다

만약 try 블록 안에서 await를 빼고 load()만 부르면 어떻게 될까요?

async function load() {
  throw new Error('연결 끊김');
}
try {
  load();
} catch (e) {
  console.log(e.message);
}
console.log('끝');
실행 결과
끝main.js:2  throw new Error('연결 끊김');        ^ Error: 연결 끊김    at load (main.js:2:9)    at main.js:5:3
  1. 5번 줄의 load()는 거부된 Promise 상자만 돌려주고 멈추지 않습니다.
  2. await로 상자를 열어 보지 않았으므로 try 블록은 아무 오류 없이 지나가고, 9번 줄의 '끝'이 먼저 찍힙니다.
  3. 지금 도는 코드가 다 끝난 뒤, 아무도 처리하지 않은 거부(Unhandled Rejection)가 남아 Node가 오류를 찍고 멈춥니다.

따라서 try / catch로 비동기 함수의 오류를 잡으려면 반드시 호출 앞에 await를 붙여야 합니다.

then 사슬에서는 .catch()로 잡기

await 대신 .then() 사슬을 쓸 때는 끝에 .catch((err) => { ... })⁠를 이어 붙여 거부된 오류를 받습니다.

async function load() {
  throw new Error('시간 초과');
}
load()
  .then((v) => {
    console.log('값:', v);
  })
  .catch((e) => {
    console.log(e.message);
  });
실행 결과
시간 초과

앞의 load()나 .then() 어디에서 오류가 나든 중간의 .then()을 건너뛰고 .catch()가 받아 처리합니다.

자주 틀리는 곳

1. await 없이 try로 감싸면 잡힌다고 생각함

가장 흔한 실수입니다. async 함수는 오류를 밖으로 바로 던지지 않고 거부된 Promise 상자에 담아 돌려줍니다. 상자를 열어 오류를 꺼내는 일은 await가 하므로, try 안에서는 늘 await를 함께 써야 합니다.

2. 잡지 않은 거부가 그 줄에서 바로 멈춘다고 생각함

await 없이 거부된 Promise를 방치하면 그 줄에서 바로 멈추지 않고, 지금 도는 동기 코드(console.log('끝'))가 끝까지 다 찍힌 뒤에야 프로그램이 멈춥니다. (한편 웹 브라우저에서는 잡지 않은 거부가 나도 콘솔에 빨간 줄만 찍힐 뿐 페이지 자체는 멈추지 않습니다.)

요약

  • async 함수 안에서 throw new Error(...)가 실행되면 거부(rejected) 상태의 Promise를 돌려준다.
  • try와 await: try 안에서 await fn()으로 기다려야 거부된 Promise가 오류로 꺼내져 catch에서 잡힌다.
  • await를 빼먹으면 try 블록을 그대로 지나쳐 버려, 지금 도는 코드가 끝난 뒤 미처리 거부 오류로 멈춘다.
  • .then() 사슬을 쓸 때는 끝에 .catch((err) => ...)를 붙여 오류를 잡는다.

연습 문제

문제 1

다음 코드를 실행했을 때 출력되는 세 줄을 순서대로 쓰세요.

async function item(id) {
  if (id <= 0) {
    throw new Error('번호 오류');
  }
  return `상품 ${id}`;
}
for (const n of [1, 0, 2]) {
  try {
    console.log(await item(n));
  } catch (e) {
    console.log(e.message);
  }
}
힌트

n = 0일 때 item(0)이 거부된 Promise를 돌려주고, await가 그 오류를 꺼내 catch (e)로 넘깁니다.

정답
실행 결과
상품 1
번호 오류
상품 2
  • n = 1일 때는 '상품 1'이 찍힙니다.
  • n = 0일 때는 await item(0)에서 오류가 던져져 번호 오류가 찍힙니다.
  • 오류가 잡혔으므로 다음 바퀴 n = 2도 돌아 '상품 2'가 찍힙니다.

문제 2

다음 코드는 fail()이 던진 '서버 점검' 오류를 catch에서 잡으려 했지만, await를 빼먹어 잡지 못하고 멈췄습니다. 5번 줄 한 줄만 고쳐 서버 점검이 찍히게 만드세요.

async function fail() {
  throw new Error('서버 점검');
}
try {
  fail();
} catch (e) {
  console.log(e.message);
}
힌트

거부된 Promise가 try 안에서 오류로 풀리도록 fail() 앞에 await를 붙이세요.

정답
  • 고친 줄: await fail();
async function fail() {
  throw new Error('서버 점검');
}
try {
  await fail();
} catch (e) {
  console.log(e.message);
}
실행 결과
서버 점검

문제 3

재고 qty가 0이면 '품절' 오류를 던지고 아니면 '주문 완료'를 돌려주는 async 함수 order(qty)가 있습니다. try / catch와 await를 써서 order(0)을 부르고, 오류가 나면 e.message를 출력하는 코드를 완성하세요.

품절
async function order(qty) {
  if (qty === 0) {
    throw new Error('품절');
  }
  return '주문 완료';
}
// 여기에 try / catch와 await로
// order(0)을 부르는 코드를 쓰세요
힌트

try 안에서 const res = await order(0);⁠을 실행하고, catch (e)에서 console.log(e.message);⁠를 실행합니다.

정답
async function order(qty) {
  if (qty === 0) {
    throw new Error('품절');
  }
  return '주문 완료';
}
try {
  const r = await order(0);
  console.log(r);
} catch (e) {
  console.log(e.message);
}
실행 결과
품절

끝까지 내려오면 저절로 표시됩니다. 이 기기에만 저장합니다.

자바스크립트 목차로