본문 바로가기

[번역] Error handling with Async/Await in JS

 

본 포스트는 Ian SegersError handling with Async/Await in JS를 번역한 글입니다.
 

Error handling with Async/Await in JS

Learn error handling in JS.

itnext.io

이 글은 다른 개발자들과 토론과 코드리뷰 동안 이해한 짤막한 글입니다. 본 내용은 주니어 JS 개발자들에게 초점이 맞춰져있습니다.

 

A Simple Try Catch

간단한 try...catch 예시로 시작해보겠습니다.

다음은 예상한대로 동작할 것입니다. 일반 에러를 throw하는 thisThrows()를 호출하면 이 에러를 catch하고 log할 수 있습니다. 그리고 finally 블록에서 선택적으로 어떤 코드를 실행할 것입니다. 그다지 특별한 것은 없습니다.

A Try Catch with a Rejecting Promise

이제 thisThrows()를 수정하여 일반 에러 대신에 promise로 reject시켜보겠습니다. 간단하게 thisThrows()를 async 함수로 만들겠습니다. async함수는 항상 promise를 반환한다는 것을 기억하세요.

  • return 구문이 정의되지 않거나 어떤 값을 반환하지 않는 return 구문에서는 resolve한 promise를 반환합니다. 이는 Promise.resolve()를 반환하는 것과 동일합니다.
  • return 구문이 어떤 값을 반환한다면 주어진 값의 resolve한 promise를 반환할 것입니다. 이는 Promise.resolve("My return String")을 반환하는 것과 동일합니다.
  • 에러가 throw되었을 때, reject된 promise가 throw된 에러와 함께 반환됩니다. 이는 Promise.reject(error)와 동일합니다.

이제 우리는 전통저인 문제점을 볼 수 있습니다. thisThrows()는 reject한 promise를 반환하고 일반 try...catch는 더이상 에러를 catch할 수 없습니다. thisThrows()가 async이기 때문에 우리가 이 함수를 호출할 때 thisThrows()는 promise를 dispatch하고 코드는 더이상 기다리지 않습니다. 따라서 finally 블록이 먼저 실행되고 그 후에 promise가 실행되고 reject됩니다. 그러므로 더이상 reject된 promise를 처리하는 어떤 코드도 존재하지 않습니다.

 

우리는 이 상황을 두 가지 방법으로 해결할 수 있습니다.

  • thisThrows()를 async 함수 내에서 호출하고, thisThrows() 함수를 await으로 기다립니다.
  • thisThrows() 함수를 .catch() 호출과 함께 체이닝합니다.

첫 번째 해결책은 다음과 같습니다.

두 번째 해결책은 아래처럼 작성할 수 있습니다.

두 해결책 모두 잘 동작합니다. 하지만 async/await 방법이 어떤 면에서는 더 쉽습니다. (개인적인 의견입니다.)

Caveats

이제 에러가 발생했을 때와 발생하지 않았을 때의 간단한 에러를 처리해보겠습니다. 다음은 특이한 케이스들을 봅시다.

Returning from an async function

어려운 문제부터 시작해봅시다. 아래의 코드는 어떤 결과를 낼까요?

처음엔, 아마 다음과 같은 결과가 나올거라고 예상할 것입니다.

We do cleanup here
Nothing Found

하지만, 이 코드는 UnhandledPromiseRejection에러가 발생합니다. 코드를 단계적으로 분석해봅시다.

  • thisThrows()는 async 메소드입니다.
  • thisThrows() 내부에서 에러가 throw되었습니다.
  • thisThrows()는 async이기 때문에 throw된 에러는 함수로부터 reject된 promise로서 반환됩니다.
  • myFunctionThatCatches() 내의 reject된 promise를 return 구문으로 반환하였습니다.
  • reject된 promise는 await 키워드에 걸리고 await 키워드는 promise가 "reject"된 상태로 resolve되었다는 것을 알고 에러를 unhandled promise rejection으로 전파합니다.

구성된 코드를 기반으로, 에러는 try...catch 구문을 지나쳐버리고 콜 트리 상의 저 멀리로 전파됩니다. 좋지 않습니다!

우리는 이 문제를 await 키워드를 return 구문에 사용함으로써 해결할 수 있습니다.

이제 try...catch는 예상대로 동작합니다. 7번째 줄의 await 키워드가 먼저 thisThrows()의 반환된 promise를 기다리고 이 promise가 reject되면 에러는 8번째 줄의 catch 블록으로 전파됩니다. 문제가 해결되었습니다!

Resetting your stack trace

이 사용 사례들을 해결하는 방법은 경우에 따라 다르므로 일반적으로 할수 있는 실수들을 확인하고 그 문제가 여러분과 관련이 있는지를 결정해야 합니다.

 

아래 코드처럼 어디선가 에러를 catch하고 이 에러를 새로운 에러로 감싸는 경우는 흔치 않습니다.

스택 추적*(stack trace)은 원래 예외를 catch한 곳부터 시작해야 한다는 것을 기억하세요. 2번째 줄에서 에러를 생성하고 9번째 줄에서 에러를 catch했을 때, 단지 원래 에러의 메세지만을 유지한 채 TypeError라는 새로운 유형의 에러를 생성했기 때문에 기존의 스택 추적을 잃게 됩니다. (심지어 가끔은 단지 메세지만을 유지하는 게 아닐 수도 있습니다)

 

* : 에러가 throw-catch되는 흐름을 추적하는 것을 말합니다. (by 역자주)

 

thisThrows() 함수가 더 많은 로직을 수행한다고 상상해보세요. 그리고 어디선가 에러가 throw되면 우리는 더이상 지금까지 로깅된 기존의 스택 추적을 볼 수 없습니다. 새로운 에러를 생성함으로써 완전 새로운 스택 추적을 생성했기 때문이죠. 만약 기존의 에러를 다시 throw할 뿐이었다면 우리는 이런 문제를 겪지 않습니다.

이제 스택 추적은 실제 에러가 발생한 곳인 스크립트의 2번째 줄부터 추적을 시작합니다. 

 

에러를 처리할 때 이 문제를 반드시 알고 있어야 합니다. 가끔은 이러한 처리가 바람직할수도 있지만 원래 에러가 발생한 지점을 햇갈리게 합니다. 이는 결국 문제의 근원을 디버깅하기 어렵게 합니다. 다른 에러로 감싸서 커스텀 에러를 만들어야 한다면 원래의 스택 추적을 유지하여 디버깅이 악몽으로 변하지 않도록 하세요.


Summary

  • 동기 코드에서 try...catch를 사용할 수 있습니다.
  • 비동기 코드에서 에러를 해결하는데 .catch()와 try...catch(async 함수와 결합하여) 사용할 수 있습니다.
  • promise를 try문에서 리턴할 때 try...catch 블록이 에러를 catch하길 원한다면 await하는 것을 잊지마세요.
  • 에러를 감싸서 다시 throw할 때는 기존 에러의 스택 추적을 잃을 수 있다는 것을 명심하세요.