[JavaScript] 에러 핸들링, 프라미스와 async, await

김서현·2023년 5월 26일

JavaScript 스터디

목록 보기
8/8

'try..catch'와 에러 핸들링

그러나 try..catch 문법을 사용하면 스크립트가 죽는 걸 방지하고, 에러를 ‘잡아서(catch)’ 더 합당한 무언가를 할 수 있게 됩니다.

'try...catch' 문법

‘try…catch’ 문법은 'try’와 'catch’라는 두 개의 주요 블록으로 구성됩니다.

try {
  // ...
} catch (err) {
  // 에러 핸들링
}

try...catch 동작 알고리즘
1. 먼저 try {...} 안의 코드가 실행

에러 객체

에러가 발생하면? -> 에러 상세내용이 담긴 객체 생성
catch 블록에 이 객체를 인수로 전달

try {
  //...
} catch(err) { // <- 여기서 err가 에러 객체!
  // ...
}

name: 에러 이름.
message: 에러 상세 내용 메시지
stack: 호출 스택. 에러를 유발한 중첩 호출들의 순서 정보를 가진 문자열로 디버깅 목적으로 사용됨.

선택적 'catch' 바인딩

에러에 대한 자세한 정보가 필요하지 않으면, catch에서 에러 객체 생략

try {
  // ...
} catch { // <-- (err) 없이 쓸 수 있음
  // ...
}

'try...catch' 사용하기

잘못된 형식의 json이 들어온 경우, JSON.parse는 에러를 만들기 때문에 스크립트가 죽는다!

=>try..catch를 사용해 이를 처리해 봅시다.

let json = "{ bad json }";

try {

  let user = JSON.parse(json); // <-- 여기서 에러가 발생하므로
  alert( user.name ); // 이 코드는 동작하지 않습니다.

} catch (e) {
  // 에러가 발생하면 제어 흐름이 catch 문으로 넘어옵니다.
  alert( "데이터에 에러가 있어 재요청을 시도합니다." );
  alert( e.name );
  alert( e.message );
}

throw 연산자

에러를 생성한다.

throw <error object>

내장 에러와의 호환을 위해 되도록 에러 객체에 name과 message 프로퍼티를 넣어주는 것을 권장
자바스크립트는Error, SyntaxError, ReferenceError, TypeError 등의 표준 에러 객체 관련 생성자를 지원한다.

let error = new Error(message);
// or
let error = new SyntaxError(message);
let error = new ReferenceError(message);
let error = new Error("이상한 일이 발생했습니다. o_O");

alert(error.name); // Error
alert(error.message); // 이상한 일이 발생했습니다. o_O

에러를 만들어서(발생시켜) 넘기기

let json = '{ "age": 30 }';

try {

  let user = JSON.parse(json); // <-- 에러 없음

  if (!user.name) { // name이라는 속성이 없으면 error를 생성해서 catch에서 잡을 수 있게 넘겨버림
    throw new SyntaxError("불완전한 데이터: 이름 없음");
  }

  alert( user.name );

} catch(e) {
  alert( "JSON Error: " + e.message ); // JSON Error: 불완전한 데이터: 이름 없음
}

에러 다시 던지기

catch 에서 다시 던져진(rethrow) 에러는 try..catch ‘밖으로 던져집니다’. 이때 바깥에 try..catch가 있다면 여기서 에러를 잡습니다. 아니라면 스크립트는 죽을 겁니다.

이렇게 하면 catch 블록에선 어떻게 다룰지 알고 있는 에러만 처리하고, 알 수 없는 에러는 ‘건너뛸 수’ 있습니다.

function readData() {
  let json = '{ "age": 30 }';

  try {
    // ...
    blabla(); // 에러!
  } catch (e) {
    // ...
    if (!(e instanceof SyntaxError)) {
      throw e; // 알 수 없는 에러 다시 던지기
    }
  }
}

try {
  readData();
} catch (e) {
  alert( "External catch got: " + e ); // 에러를 잡음
}

이렇게 바깥에 try...catch를 놓으면 예상치 못한 에러도 처리할 수 있다!

try...catch...finally

finally안의 코드는 다음과 같은 상황에서 실행됩니다.

에러가 없는 경우: try 실행이 끝난 후
에러가 있는 경우: catch 실행이 끝난 후
=> 항상 실행!
(return문이 있어도 실행된다..!)

finally 절은 무언가를 실행하고, 실행 결과에 상관없이 실행을 완료하고 싶을 경우 사용

ex) 피보나치 함수 fib(n)의 연산 시간을 측정하고 싶다고 해 봅시다. 함수 실행 전에 측정을 시작해서 실행이 끝난 후 측정을 종료하면 되겠죠. 그런데 함수 실행 도중 에러가 발생하면 어떻게 될까요? 아래 fib(n)에는 음수나 정수가 아닌 수를 입력할 경우 에러가 발생합니다.

이런 경우에 finally를 사용할 수 있습니다. finally 절은 무슨 일이 일어났든 관계없이 연산 시간 측정을 끝마치기 적절한 곳이죠.

fib 함수가 에러 없이 정상적으로 실행되든 에러가 발생하든 상관없이, finally를 사용하면 연산 시간을 제대로 측정할 수 있습니다.

전역 catch

try..catch에서 처리하지 못한 에러를 잡는 것은 아주 중요하기 때문에, 자바스크립트 호스트 환경 대다수는 자체적으로 에러 처리 기능을 제공합니다.

ex) Node.js의 process.on("uncaughtException")이 그 예입니다. 브라우저 환경에선 window.onerror를 이용해 에러를 처리할 수 있습니다. window.onerror 프로퍼티에 함수를 할당하면, 예상치 못한 에러가 발생했을 때 이 함수가 실행됩니다.

<script>
  window.onerror = function(message, url, line, col, error) {
    alert(`${message}\n At ${line}:${col} of ${url}`);
  };

  function readData() {
    badFunc(); // 에러가 발생한 장소
  }

  readData();
</script>

이건 스크립트를 복구하려는 것이 아니라 window.onerror는 개발자에게 에러 메시지를 보내는 용도로 사용!


프라미스와 async, await

프라미스 체이닝

.then 또는 .catch, .finally의 핸들러(어떤 경우도 상관없음)가 프라미스를 반환하면, 나머지 체인은 프라미스가 처리될 때까지 대기합니다. 처리가 완료되면 프라미스의 result(값 또는 에러)가 다음 체인으로 전달된다.

계속적으로 프라미스를 반환해서 체이닝

new Promise(function(resolve, reject) {

  setTimeout(() => resolve(1), 1000); // (*)

}).then(function(result) { // (**)

  alert(result); // 1
  return result * 2;

}).then(function(result) { // (***)

  alert(result); // 2
  return result * 2;

}).then(function(result) {

  alert(result); // 4
  return result * 2;

});

프라미스와 에러 핸들링

암시적 try…catch

보이지 않는(암시적) try..catch : 예외가 발생하면 암시적 try..catch에서 예외를 잡고 이를 reject처럼 다룬다

new Promise((resolve, reject) => {
  throw new Error("에러 발생!");
}).catch(alert); // Error: 에러 발생!
new Promise((resolve, reject) => {
  reject(new Error("에러 발생!"));
}).catch(alert); // Error: 에러 발생!

다시 던지기

catch에서 조건 외의 에러 발생 시 error를 던지고 마지막에 또 에러 처리

// 실행 순서: catch -> catch
new Promise((resolve, reject) => {

  throw new Error("에러 발생!");

}).catch(function(error) { // (*)

  if (error instanceof URIError) {
    // 에러 처리
  } else {
    alert("처리할 수 없는 에러");

    throw error; // 에러 다시 던지기
  }

}).then(function() {
  /* 여기는 실행되지 않습니다. */
}).catch(error => { // (**)

  alert(`알 수 없는 에러가 발생함: ${error}`);
  // 반환값이 없음 => 실행이 계속됨

});

처리되지 못한 거부

에러를 처리하지 못했을 때
실행 흐름은 가장 가까운 rejection 핸들러로 넘어갑니다. 그런데 위 예시엔 예외를 처리해 줄 핸들러가 없어서 에러가 ‘갇혀버립니다’.

자바스크립트 엔진은 프라미스 거부를 추적하다가 위와 같은 상황이 발생하면 전역 에러를 생성합니다. 콘솔창을 열고 위 예시를 실행하면 전역 에러를 확인할 수 있습니다.

브라우저 환경에선 이런 에러를 unhandledrejection 이벤트로 처리할 수 있습니다.
unhandledrejection 이벤트는 HTML 명세서에 정의된 표준 이벤트이다!

window.addEventListener('unhandledrejection', function(event) {
  // unhandledrejection 이벤트엔 두 개의 특수 프로퍼티가 있습니다.
  alert(event.promise); // [object Promise] - 에러를 생성하는 프라미스
  alert(event.reason); // Error: 에러 발생! - 처리하지 못한 에러 객체
});

new Promise(function() {
  throw new Error("에러 발생!");
}); // 에러를 처리할 수 있는 .catch 핸들러가 없음

프라미스 API

Promise.all

복수의 URL에 동시에 요청을 보내고, 다운로드가 모두 완료된 후에 콘텐츠를 처리할 때

Promise.all의 첫 번째 프라미스는 가장 늦게 이행되더라도 처리 결과는 배열의 첫 번째 요소에 저장됩니다.

let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/Violet-Bora-Lee',
  'https://api.github.com/users/jeresig'
];

// fetch를 사용해 url을 프라미스로 매핑합니다.
let requests = urls.map(url => fetch(url));

// Promise.all은 모든 작업이 이행될 때까지 기다립니다.
Promise.all(requests)
  .then(responses => responses.forEach(
    response => alert(`${response.url}: ${response.status}`)
  ));

Promise.all에 전달되는 프라미스 중 하나라도 거부되면, Promise.all이 반환하는 프라미스는 에러와 함께 바로 거부됩니다.

Promise.allSettled

Promise.allSettled는 모든 프라미스가 처리될 때까지 기다립니다. 반환되는 배열은 다음과 같은 요소를 갖습니다.

응답이 성공할 경우 – {status:"fulfilled", value:result}
에러가 발생한 경우 – {status:"rejected", reason:error}

Promise.race

가장 먼저 처리되는 프라미스의 결과(혹은 에러)를 반환합니다.

Promise.race([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("에러 발생!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1

Promise.resolve와 Promise.reject

프라미스 메서드 Promise.resolve와 Promise.reject는 async/await 문법(뒤에서 다룸)이 생긴 후로 쓸모없어졌기 때문에 근래에는 거의 사용하지 않습니다.

Promise.resolve

Promise.resolve(value)는 결괏값이 value인 이행 상태 프라미스를 생성합니다.

Promise.reject

Promise.reject(error)는 결괏값이 error인 거부 상태 프라미스를 생성합니다.


async와 await

async

async가 붙은 함수는 반드시 프라미스를 반환하고, 프라미스가 아닌 것은 프라미스로 감싸 반환합니다.

await

async 함수 안에서만 동작.
프라미스가 처리될 때까지 기다립니다. 결과는 그 이후 반환됩니다.

에러 핸들링

async function f() {

  try {
    let response = await fetch('http://유효하지-않은-주소');
  } catch(err) {
    alert(err); // TypeError: failed to fetch
  }
}

f();
async function f() {
  let response = await fetch('http://유효하지-않은-주소');
}

// f()는 거부 상태의 프라미스가 됩니다.
f().catch(alert); // TypeError: failed to fetch // (*)
  • Promise.all도 함께 쓸 수 있다.
// 프라미스 처리 결과가 담긴 배열을 기다립니다.
let results = await Promise.all([
  fetch(url1),
  fetch(url2),
  ...
]);

0개의 댓글