[Javascript] throw, try, catch, async, await 기초

박기영·2022년 12월 6일
0

Javascript

목록 보기
25/45

리액트를 사용하든, JS를 사용하든 api 통신을 할 때
axios나 fetch 등을 활용하게 된다.
사용법의 이해를 위해 예제들을 찾아보면 정말 많은 예시들이
throw, try, catch, async, await를 섞어서 사용한다.
이는 도대체 무슨 의미이며, 어떤 효과를 가지는지 알아보자.

기초적인 내용을 학습하기 위한 내용이므로 흐름에 따라 한번에 몰아서 작성했습니다

throw

function testThrow(x, y) {
  if (typeof x !== "number" || typeof y !== "number") {
    throw "좀 이상한데요?";
  }

  return x + y;
}

console.log(testThrow(1, 3));

위와 같은 코드를 실행시켜보자.
보아하니 문제 없이 작동할 것 같다.

그렇다. 잘 작동한다.
throw를 실험하기 위해 일부러 에러를 내보자.

function testThrow(x, y) {
  if (typeof x !== "number" || typeof y !== "number") {
    throw "좀 이상한데요?";
  }

  return x + y;
}

console.log(testThrow("1", 3));

숫자형이 아닌 문자형을 넣어 에러를 발생시켜봤다.

throw에 넣어준 에러 메세지가 콘솔에 뜨는 것을 볼 수 있다.

참고로 필자는 codesandbox를 사용했는데,
크롬 브라우저에서 개발자 도구로 살펴보면 아래와 같이 나온다.

try, catch, Error 객체

이번에는 아래와 같은 코드로 실험을 해보자.

function f2() {
  console.log("f2 start");
  console.log("f2 end");
}

function f1() {
  console.log("f1 start");
  f2();
  console.log("f1 end");
}

console.log("will : f1");
f1();
console.log("did: f1");

아래와 같이 실행된다.

여기에 일부러 에러를 발생시켜보겠다.

function f2() {
  console.log("f2 start");
  throw "에러";
  console.log("f2 end");
}

아래와 같이 결과가 나온다.

이제 예외 처리를 해보자.
try, catch를 사용하여 에러가 발생할 수 있는 부분을 처리해주자.

function f1() {
  console.log("f1 start");

  try {
    f2();
  } catch (err) {
    console.log(err);
  }

  console.log("f1 end");
}

만약 f2()에서 에러가 발생하면 catch를 해서 콘솔에 보여주겠다는 것이다.
결과를 살펴보자.

오! 이번에는 에러가 발생했지만 코드가 중지되지않았다.
예외 처리한 부분에서 에러에 대한 반응을 수행하며 f2()를 종료한 것을 제외하면,
그대로 나머지 코드를 진행했다.

이번엔 예외 처리 부분을 바꿔보자.

function f2() {
  console.log("f2 start");
  throw "에러";
  console.log("f2 end");
}

function f1() {
  console.log("f1 start");
  f2();
  console.log("f1 end");
}

console.log("will : f1");
try {
  f1();
} catch (err) {
  console.log(err);
}
console.log("did: f1");

f1() 수행 중 에러가 발생하는 것은 f2() 부분이다.
따라서 f1() 내에 있는 f2()에서 에러를 catch해서 예외처리를 하므로,
에러를 마주하기 전까지는 실행하고, 에러를 발견한 뒤부터는 catch가 실행된다.
따라서 다음과 같은 결과가 나온다.

Error 객체

그런데 보통 throw를 사용할 때 위 예시처럼 문자열을 적지않고
Error 객체를 사용한다.

function f2() {
  console.log("f2 start");
  throw new Error("에러");  // Error 객체 사용
  console.log("f2 end");
}

function f1() {
  console.log("f1 start");

  f2();

  console.log("f1 end");
}

console.log("will : f1");
try {
  f1();
} catch (err) {
  console.log(err);
}
console.log("did: f1");

뭐가 달라질까?
결과를 살펴보자.

짠! 에러가 발생한 곳의 정보가 적혀있는 것을 볼 수 있다.(call stack의 정보)

promise.catch()

다음과 같은 함수를 실행시켜보자.

function wait(sec) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("error!");
    }, sec * 1000);
  });
}

wait(3);

예상되는 결과는 3000ms 뒤 에러가 reject되는 것이다.(여기서는 의도적으로 에러 만든 것임)

실제로 3초 뒤 아래와 같은 결과가 나왔다.

코드에서는 의도적으로 에러를 발생시킨 것이지만,
결국 이는 예외 처리를 하지 않았기 때문에 발생한 것이라고 생각할 수 있겠다.

in promise라고 적혀있다. 이를 이용해서 예외 처리를 해보도록 하자.

위에서 배운 try, catch를 활용하겠다.

function wait(sec) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("error!");
    }, sec * 1000);
  });
}

try {
  wait(3);
} catch (err) {
  console.log(err);
}

자, 과연 예외 처리가 될까?

아니다. 도대체 왜 처리가 안된걸까?
wait()는 비동기적으로 실행되고 있기 때문이다.

예외가 발생하는 타이밍이 try가 감싸고 있는 코드가 실행되는 타이밍과 달라,
콜스택이 비었을 때 예외가 발생해버려서 처리가 안되는 것이다.

그러면...Promise에서 발생하는 예외는 어떻게 처리해요?
Promise.catch()를 여기서 사용한다.

try, catch문을 아래와 같이 수정했다.

wait(3).catch((err) => console.log(err));

과연 reject로 발생한 에러를 처리할 수 있을까?

예외 처리가 된 것을 확인할 수 있다.

여담으로 Promisethen을 활용하여, 성공 시 추가적으로 작동할 코드를 연결해줄 수 있다.
그래서 then은 여러번 사용이 가능하다.

wait(3)
  .then(() => console.log("1st then"))
  .then(() => console.log("2st then"));

이런 식의 코드가 가능하다는 뜻이다.
그런데, catch는 이게 불가능하다.

wait(3)
  .catch((err) => console.log("1st err", err))
  .catch((err) => console.log("2nd err", err));

대충 보면 두 번의 콘솔이 찍힐 것 같지만,

실행되는건 첫 번째 catch 뿐이다.
이유가 뭘까?

보통 이런 체인 구조에서는 첫 번째 then이나 두 번째 then이나 계속 같은 객체를 리턴하는데 반해,
Promise는 이런 구조에서 then이나 catch에서 전부 다른 객체를 리턴하기 때문이다.

정확히 어디서 어떤 객체가 리턴되는건지 알아보자.

function wait(sec) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("error!");
    }, sec * 1000);
  });
}

위 코드에서 Promise 내부에 있는 것들이 wait()를 실행했을 때,
즉, 아래 코드에서 리턴된다.

wait(3)

이제 이 Promise에서 예외가 발생하면

.catch((err) => console.log("1st err", err))

를 실행한다.

그런데 이 친구는 방금 실행했던 wait()에서 발생한 Promise에서의 객체와는 관련이 없고,
잘 작동을 했는지에 대해서만 예외처리를 담당하는 것이다.
즉, 첫 번째 catch가 반환하는 것은 catch 자체에서 동작이 제대로 됐는지에 해당하는 Promise인 것이다.

그러면 얘는 뭘까?

.catch((err) => console.log("2nd err", err));

두 번째 catch는 첫 번째 catch가 잘 동작했는지에 대한 예외 처리를 담당하는 것이 되기 때문에
이미 잘 작동한 첫 번째 catch에 대해서 처리할 것이 없으므로,
아무 일도 발생하지 않는 것이다.

정리하면 이렇다.
처음 함수 실행에서의 Promise가 계속해서 넘겨지는게 아니라
각 단계에서의 Promise가 넘겨지는 것이므로,
then, catch가 같은 Promise를 기준으로 작동하는 것이라고 생각하면 안된다!!!!
그냥 본인이 실행되기 바로 직전 단계에 있는 Promise를 기준으로 작동하는 것이다.

그러면 이런 의문이 생길 것이다.
"그럼 첫 번째 catch에서의 에러를 계속해서 에러 상황으로 봐야하고,
또 사용해야될 때는 어떻게 해야하나요?"

그럴 때는, 아래와 같이 사용하면 된다.

wait(3)
  .catch((err) => {
    console.log("1st err", err);

    throw err;
  })
  .catch((err) => console.log("2nd err", err));

throw를 통해 에러를 다시 던져주면, 아래와 같이 실행된다.

또 다른 방법이 있는데, 바로 then을 이용하는 방법이다.
then에 대한 설명을 보면 다음과 같다.

onfulfilledonrejected를 입력해줄 수 있다는 것이다.
전자는 then에서 어떤 것을 성공했을 때의 경우 실행되는 것이고,
후자는 어떤 것을 실패했을 경우 실행되는 것이다.

이를 활용하여 예시 코드를 아래와 같이 수정해볼 수 있겠다.

wait(3)
  .then(
    () => {
      console.log("done!!!");
    },
    (err) => {
      console.log("1st err in then", err);
    }
  )
  .catch((err) => console.log("2nd err", err));

오! onrejected에 넣어놓은 함수가 실행된 것을 볼 수 있다.
그런데, 두 번째 catch는 실행되지 않았다.
왜? 이미 then에서 예외처리마저 성공적으로 해냈기 때문에
catchthenPromise를 성공적으로 완료됐다고 판단해서 그런 것이다.

그럼에도 불구하고 then에서의 에러를 이어 가져가서 catch에서 사용하고자 한다면,

wait(3)
  .then(
    () => {
      console.log("done!!!");
    },
    (err) => {
      console.log("1st err in then", err);
      throw new Error("throw in then");
    }
  )
  .catch((err) => console.log("2nd err", err));

짠! then에서의 예외 처리가 성공되었음에도,
catch까지 그 에러를 끌고 가서 처리해줄 수 있다!

async, await

우선 async에 대해서 기초적인 것을 살펴보고 가자.

async function myAsyncFun() {
  return "done!";
}

const result = myAsyncFun();
console.log(result);

resultmyAsyncFun()이 반환하는 done!을 보여줄 것으로 예상된다.
그러나...

콘솔에 찍힌 것은 Promise이다!
즉, async를 사용하면 Promise가 반환된다는 것이다.

이는,

function myPromiseFun() {
  return new Promise((resolve, reject) => {
    resolve("done!");
  });
}

const result2 = myPromiseFun();
console.log(result2);

위 코드와 동일하게 작동하는 것이다.
위 코드도 콘솔에 완전히 동일한 결과가 출력된다.

async 함수에서의 returnPromise 함수에서의 resolve에 해당한다고 생각하면 된다.

이번엔 에러를 발생시켜보자.

function myPromiseFun() {
  return new Promise((resolve, reject) => {
    reject("myError!");
  });
}

const result2 = myPromiseFun();
console.log(result2);

myAsyncFun()에서도 에러를 발생시키고 싶다면 어떻게 해야할까?

async function myAsyncFun() {
  throw "myAsyncError!";
}

const result = myAsyncFun();
console.log(result);

throw를 사용하면 된다. 그러면 동일하게 Promise 에러가 발생한다.
이 에러를 잡아서 처리하고자 한다면 catch()를 사용하면 된다.

async function myAsyncFun() {
  throw "myAsyncError!";
}

const result = myAsyncFun().catch((err) => {
  console.log(err);
});

function myPromiseFun() {
  return new Promise((resolve, reject) => {
    reject("myError!");
  });
}

const result2 = myPromiseFun().catch((err) => {
  console.log(err);
});

짠! 에러를 잡아서 처리한 것을 볼 수 있다.

이번에는 await를 알아보자. awaitasync 내에서 사용할 수 있다.
await는 뭐하는 녀석일까?
바로 Promise를 기다리는 녀석이다!
Promise가 완전히 fulfilled 되거나 rejected 되기를 기다리는 것이다.

음...와닿지 않는다.
예시를 살펴보자.

function wait(sec) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("done!");
    }, sec * 1000);
  });
}

async function myAsyncFun() {
  console.log(new Date());
  wait(3);
  console.log(new Date());
}

const result = myAsyncFun();

어라..? 3초를 기다릴 거라고 생각했는데 그냥 바로 new Date()가 실행되어버렸다.
이는 wait()가 비동기이기 때문에 발생한 것이다.
wait()를 놔두고 바로 다음 코드를 실행한다는 것이다.

이제 await를 사용해보자. 기다려!

function wait(sec) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("done!");
    }, sec * 1000);
  });
}

async function myAsyncFun() {
  console.log(new Date());
  await wait(3);
  console.log(new Date());
}

const result = myAsyncFun();

짠! 기다려!를 외쳤더니 3초를 기다리고 실행이 되었다. 착하네요.
awaitPromise를 기다린다는 것을 증명했다!

이번에는 reject로 실험해보자.

function wait(sec) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("wait Error!");
    }, sec * 1000);
  });
}

async function myAsyncFun() {
  console.log(new Date());
  await wait(3);
  console.log(new Date());
}

const result = myAsyncFun();

예상되는 동작은 new Data()가 실행되고, 3초 기다렸다가 reject가 되는 것이다. 확인해보자.

짠! 예상대로 작동했다.
reject에 들어있던 값이 throw된 것도 확인했다.

이왕 throw까지 해봤으니 try catch를 사용해보자.

function wait(sec) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("wait Error!");
    }, sec * 1000);
  });
}

async function myAsyncFun() {
  console.log(new Date());

  try {
    await wait(3);
  } catch (err) {
    console.log(err);
  }

  console.log(new Date());
}

const result = myAsyncFun();

첫 번째 new Date()가 실행되고,
3초 기다린 뒤 wait()가 실행되며 rejectPromise가 예외 처리가 된다.
그 후, 두 번째 new Date()가 실행된다.

try catch를 했기 때문에 예외 처리가 되서, 두 번째 new Date()가 실행된 것이다.
catch를 안 썼다면 예외 처리 없이 그냥 에러만 발생했을 것이다.

물론 코드를 아래와 같이 사용해도 된다.

function wait(sec) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("wait Error!");
    }, sec * 1000);
  });
}

async function myAsyncFun() {
  console.log(new Date());

  await wait(3).catch((err) => {
    console.log(err);
  });

  console.log(new Date());
}

const result = myAsyncFun();

완전히 동일하게 작동한다.

그런데, 이 방법은 return되는 것이 있다면 주의해야한다.

function wait(sec) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("done!");
    }, sec * 1000);
  });
}

async function myAsyncFun() {
  console.log(new Date());

  const result = await wait(3).catch((err) => {
    console.log(err);
  });

  console.log(result);
  console.log(new Date());
}

const result = myAsyncFun();

정상적으로 resolve에 들어있던 값이 return이 되었다.
그 값은 result에 저장이 되어서 콘솔에 찍힌 것이다.

그런데 만약에 reject 상황에서는 어떻게 될까?

function wait(sec) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("wait Error");
    }, sec * 1000);
  });
}

async function myAsyncFun() {
  console.log(new Date());

  const result = await wait(3).catch((err) => {
    console.log(err);
  });

  console.log(result);
  console.log(new Date());
}

const result = myAsyncFun();

어라, undefined가 찍히는 것을 확인 할 수 있다.
왜 그럴까?

await가 기다리고 있었던 Promisewait()에서의 Promise가 아니라
catch()를 통해 returnPromise이기 때문이다.

현재 catch()에서는 어떤 것도 return하지 않는다.
return을 하는게 있어야 그게 바로 catch()resolve 값이 되는데,
return을 하지 않기 때문에 undefined가 콘솔에 찍힌 것이다.

지금까지는 우리가 의도적으로 발생시킨 에러에 대해서만 살펴봤는데,
이번에는 의도한게 아닌(문법, 오타 등등...) 에러에 대해서는 어떻게 되는지 살펴보자.

function wait(sec) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("wait Error");
    }, sec * 1000);
  });
}

async function myAsyncFun() {
  consoooooole.log(new Date());

  const result = await wait(3).catch((err) => {
    console.log(err);
  });

  console.log(result);
  console.log(new Date());
}

const result = myAsyncFun();

console.log() 부분에 아주 명확하게 잘 보이는 오타 에러가 존재하고 있다.

음~ 그러면 try catch하면 예외 처리 할 수 있겠네~

try {
  myAsyncFun();
} catch (err) {}

그런데...예외 처리가 안된다...똑같이 저 에러가 나온다.
왜?
myAsyncFun()Promise를 리턴했기 때문이다.
따라서 이 에러를 잡고자한다면

try {
  myAsyncFun().catch((err) => {});
} catch (err) {}

이런 식으로 작성해야한다.
그랬더니 예외 처리가 잘된다! Uncaught 에러가 사라졌다.

참고 자료

코드종님 유튜브 강의

profile
나를 믿는 사람들을, 실망시키지 않도록

0개의 댓글