Error Handling - Async & Await

DatQueue·2022년 3월 24일
1

포스팅 시작 전 ...

지난 async&await 첫 포스팅에서 원래 Error Handling 관련해서 설명을 하려했지만 async&await의 Error Handling을 공부하면서 상당히 많은 생각들을 할 필요가 있었고 특히 Promise와는 다른 예외처리 과정에 있어 남겨야 할 내용이 너무 많다고 판단하였다.

그래서 이렇게 Async & Await의 예외처리는 따로 포스팅을 준비하였다. 이 포스팅을 읽기 전 Promise의 예외처리방법에 대해서 선행학습이 된 후 오기 바란다.

그럼 시작하겠다.

Error Handling (async&await의 예외처리 방법)

우린 지난 포스팅 ( new Promise 포스팅 참고 )에서 Promise의 Error Handling에 대해서 알아보았다. 간단히 설명을 조금 해보자면 Promise 생성자 함수의 parameter중 reject로 리턴된 값은 catch( ) 메서드를 통해 예외처리를 해주었다.

예를 들자면

function promiseFunc() {
  return new Promise((resolve, reject) => {
    reject("done!");
  });
}
promiseFunc().catch((e) => {
  console.error(e);
});

아주 간단한 코드로 Promise의 예외처리를 해보았다. 결과를 확인해보면

다음과 같이 console.error를 통해 reject의 예외처리를 하였다.
(참고로 console.log(e)를 호출시 에러없이 done!이 출력된다.)

그럼 이번엔 동일한 코드를 async-await으로 바꿔보자. 어떻게 해야할까?

async 함수에는 Promise 함수와 달리 resolve, reject라는 값이 없다. 에러가 없는 한 return값은 Promise의 resolve값과 같게 되고 우린 await 키워드를 통해서 진행해나갔다.
그렇지만 rejected상태의 값은 어떻게 처리할 수 있을까?

바로, 직접 에러를 발생시켜줘야한다. "throw" 를 사용하는 것이다.

throw

throw문은 사용자 정의 예외를 발생(throw)할 수 있다. 예외가 발생하면 현재 함수의 실행이 중지되고 ( throw 이후의 명령문은 실행되지 않는다. ), 제어 흐름은 콜스텍의 첫 번째 catch 블록으로 전달된다. 호출자 함수 사이에 catch 블록이 없으면 프로그램이 종료된다.

( throw에 관한 자세한 내용은 주제와 벗어나므로 다루지 않겠습니다. 추 후 < 예외 처리 > 포스팅을 따로 만둘어 다룰 예정입니다. )

이제, throw를 이용해서 위의 Promise 구문과 같은 기능의 async-await 함수를 만들어보자.

async function asyncFunc() {
  throw "myAsyncError !";              // throw
}

asyncFunc().catch((e) => console.error(e));

결과를 확인해보면

여기서 재미있는 사실은 결국 async도 Promise를 return하기 때문에 catch( ) 메서드를 사용하여 에러처리가 가능하다는 점이다.

참고로 일반적 에러를 띄울때 " throw 표현식 " 으로 바로 가지않고,

async function asyncFunc() {
  throw new Error("myAsyncError");       //throw new Error
}

asyncFunc().catch((e) => console.error(e));

"new Error" 라는 Javascript method를 이용해 에러를 띄운다.
이때 결과는 다음과 같다.

new Error 객체와 함께 발생한 오류는 확장 할 때 스택 추적을 제공한다. 위 결과에서도 Error 표현구문이 뜬 후, 어디서 에러가 발생했고 어떤 함수에 포함되있는지의 유용한 정보를 제공해준다. 이는 코드 디버깅에 있어서 굉장히 좋은 정보이다.

async&await만의 예외처리 (try & catch)

다음 코드를 살펴보자.

function wait(sec) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("done!");                //fullfilled
    }, sec * 1000);
  });
}
async function asyncFunc() {
  console.log(new Date());
  await wait(3);
  console.log(new Date());
}

asyncFunc();

Promise를 return한 wait( )이란 함수를 async함수인 asyncFunc( )안에서 await을 통해 호출하였다.

await 전의 new Date이 먼저 나오고 3초 뒤에 다음 new Date이 출력된다.
이건 resolve상황 . 즉, "fullfilled" 상태이므로 가능하다.

그렇다면 "rejected"상태 일때는 어떻게 될까?

function wait(sec) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("done!");               //rejected
    }, sec * 1000);
  });
}
async function asyncFunc() {
  console.log(new Date());
  await wait(3);
  console.log(new Date());
}

asyncFunc();


( runtime error는 무시 )
await 이후의 new Date에선 Error가 나오는 것을 확인할 수 있다. Promise의 "reject"가 async 에러처리인 "throw"와 같이 동작한 것이다.

이 에러를 한번 잡아보자.

우린 "Javascript 예외처리"에 있어서 고유의 방법인 try...catch 구문을 사용할 것이다.
try...catch에 대해 깊게 다루기엔 너무 장황해지므로 간단한 문법만 설명하겠다.

보다 자세한 내용을 알고 싶으면 MDN - try...catch MDN 공식 사이트를 참조하기 바란다.

문법

try{
  try_statements
}
[catch(exception_var){
  catch_statements
}]
[finally{
  finally_statements
}]

다음과 같은 구조로 try...catch문이 구성된다.

  • try_statements : 실행될 선언들
  • catch_statements : try 블록에서 예외가 발생했을 때 실행될 선언들
  • exception_var : catch 블록과 관련된 예외 객체를 담기 위한 식별자
  • finally_statements : try 선언이 완료된 이후에 실행된 선언들. 이선언들은 예외 발생 여부와 상관없이 실행된다.

다시 앞전의 코드로 돌아가서 try...catch문을 적용시켜보겠다.

function wait(sec) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("wait Error!");
    }, sec * 1000);
  });
}
async function asyncFunc() {
  console.log(new Date());
  try {
    await wait(3);
  } catch (e) {
    console.log(e);
  }
  console.log(new Date());
}

asyncFunc();

결과를 확인해보면

다음과 같이 처음 날짜가 나오고 3초 뒤 catch문에서 잡은 reject값이 나옴과 동시에 다음 날짜가 나온다.
만약, try..catch문을 사용하지 않았더라면 그 다음 Date는 출력되지 못한다.

그런데 곰곰히 생각해보자.

분명 await뒤의 wait함수는 Promise를 return하게 되니까 굳이 try...catch문이 아닌 Promise.catch를 사용해도 되지 않을까?

이런 생각을 다들 하셨을 것이다.

그럼 그렇게 코드를 짜보자.

function wait(sec) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("wait Error!"); //rejected
    }, sec * 1000);
  });
}
async function asyncFunc() {
  console.log(new Date());
  await wait(3).catch(console.log);   //catch
  console.log(new Date());
}

asyncFunc();


결과는 try...catch 문을 사용했을 경우와 동일하다.

그럼에도 try...catch문을 사용하는 이유?

방금 위에서 예외 처리를 하는 과정에 있어 try...catch문 대신 Promise객체의 catch메서드를 사용해보니 try...catch를 사용하였을때와 같은 결과를 얻을 수 있었다.

위의 두 코드를 비교해보았을 때 심지어 Promise의 catch메서드를 사용해 예외처리를 진행한 코드가 한 줄 밖에 되지 않아 훨씬 짧은 것을 확인 할 수도 있다.

그렇다면 try...catch문을 사용하는 것이 손해일까?

이 부분은 사실 작성자 본인도 아직까지 완벽히 이해하지 못하였다. 하지만 이 부분에 대해 공부를 하면서 나름 느끼고 정립한 개념을 위주로 생각을 정리해보고자 한다.

우리가 Promise객체 구조를 작성해보면서 then과 catch메서드의 복잡성과 불편함을 async-await을 통해 해결하는 과정을 가져보았다. 그 결과 복잡한 비동기 코드를 훨씬 더 단순한 동기적 코드로 만들 수 있었다. 일반적 '동기적' 코드에선 우린 try...catch문을 이용해 예외처리를 한다.
즉, 동기/비동기 구분없이 try...catch로 일관되게 예외처리를 할 수 있는 것이 바로 async-await의 최대 이점이 되는 것이다.

좀 주저리주저리 말했는데 우린 위 예시코드들이나 샘플코드같은 아주 짧은 코드를 작성할 일은 거의 없다. 실제적 프로젝트 코드로 넘어가는 순간 간단한 구조가 아닌 아주 복잡한 구조의 비동기 처리 코드를 작성하게 될 것이다. 또한 비동기와 동기가 섞여있는 구조일지도 모른다. 그러한 상황에서 우린 자연스럽게 async-await을 선택할 것이고 그에 따라 try...catch를 사용하게 될 것이다.

마무리 ...

지금까지 async&await의 예외처리에 대해 알아보았다. 솔직히 작성자 본인도 정확히 알지 못하고 포스팅을 마무리 짓는거 같다. 누군가 이 포스팅을 보고 나에게 정확한 사실과 개념을 알려주면 좋겠다는 생각이 문득 든다. 이 포스팅이 잘못됫다고 말하는 분이 있으면 오히려 너무 감사할 것 같다. 사실 정보를 전달하기위한 포스팅에서 이런 말을 적는다는 것이 모순이긴 하지만 모두들 정확한 개념과 사실을 정립하기 위해선 쓴소리는 꼭 필요하다 생각한다.
여하튼, 이번 포스팅은 이렇게 짧게 마무리 짓겠다.

다음 포스팅부턴 async&await과 순수 Promise객체에 대해 비교해보는 시간을 가질까 한다.

profile
You better cool it off before you burn it out / 티스토리(Kotlin, Android): https://nemoo-dev.tistory.com

0개의 댓글