Error 객체

자바스크립트의 내장된 Error 객체를 사용해서 간편한 에러 처리를 할 수 있다.

const err = new Error('invalid email');

Error 인스턴스는 에러와 통신하는 수단으로 Error 인스턴스를 만들어 에러가 일어나는 상황을 추측해 대처하는 방법으로 사용한다.

이메일 유효성 검사 에러

function validateEmail(email) {
    return email.match(/@/) ?
        email : 
        new Error(`invalid email: ${email}`);
}

try/catch와 예외 처리

Error 인스턴스는 예외 처리에서 더 자주 사용된다.

예외 처리는 에러를 컨트롤하는 메커니즘으로, 예상치 못한 상황에 대처하는 방식이다.

예외 처리는 try...catch 문을 사용하고, 예외가 있으면 그것을 catch한다는 뜻이 잘 드러나있다.
이전 예제에서 사용자가 이메일 주소에서 @을 빼먹은, 예상할 수 있는 에러는 처리할 수 있지만 만약 프로그래머의 부주의로 email에 문자열이 아닌 어떤 것을 할당하게 된다면 에러를 일으키고 프로그램은 설명 없이 멈춰버리게 된다. 이렇게 예상치 못한 상황을 try...catch 문으로 감싸 사용하면 된다.

const email = null;

try {
    const validatedEmail = validateEmail(email);
    if(validatedEmail instnceof Error) {
        console.error(`Error: ${validatedEmail}`);
    } else {
        console.log(`Valid email: ${validatedEmail}`);
    }
} catch(err) {
    console.error(`Error: ${err.message}`);
}

에러를 캐치했으므로 프로그램은 멈추지 않고, 에러를 계속 기록하며 진행할 수 있게된다. 문제가 해결되진 않았지만 이유 없이 프로그램이 작동하지 않게되는 것을 막을 수 있다.

실행 흐름은 에러가 일어나는 즉시 catch블록으로 이동하고 에러가 일어나지 않으면 catch블록은 실행되지 않는다.

에러 일으키기

자바스크립트가 에러를 일으키기만 기다릴 필요 없이 직접 에러를 일으켜서 예외 처리 작업을 시작할 수도 있다.

만약 은행 어플리케이션에 사용할 현금 인출 기능을 만든다면, 계좌의 잔고가 요청받은 금액보다 적다면 에러를 일으켜야한다.

function billPay(amount, payee, account) {
    if(amount > account.balance)
        throw new Error("insufficient funds");
    account.transfer(payee, amount);
}

throw를 호출하면 함수는 즉시 실행을 멈추므로, 위 예제에서는 account.transfer가 호출되지 않으므로 잔고가 부족한데도 현금을 찾아가는 사고는 발생하지 않는다.

예외 처리와 Call Stack

자바스크립트 인터프리터는 함수의 호출 과정을 모두 추적하고 있다.
함수 a에서 함수 b를 호출하고 함수 b에서는 함수 c를 호출한다면, 함수 c가 실행을 마칠 때 실행 흐름은 함수 b로 돌아가고 b가 실행을 마칠 때 실행 흐름은 함수 a로 돌아간다.
그렇기 때문에 c가 실행 중일 때는 a와 b는 완료될 수 없고 이렇게 완료되지 않은 함수가 쌓이는 것을 Call Stack 이라 부른다.

에러는 콜 스택 어디에서든 캐치할 수 있다. 어딘가에서 일어나는 에러를 캐치하지 않으면 자바스크립트 인터프리터는 프로그램을 멈추게 되고 처리하지 않은 예외 때문에 프로그램이 충돌하는 원인이 된다.

에러를 캐치하면 콜 스택에서 문제 해결에 유용한 정보를 얻을 수 있다.
만약 함수 c에서 에러가 일어났다면, 콜 스택은 c에서 일어난 에러를 보고하는 데 그치지 않고 b가 c를 호출했으며, b는 a에서 호출했다는 것도 함께 알려주기 때문에 디버그에 유용하게 사용된다.

function a() {
    console.log('a: calling b');
    b();
    console.log('a: done');
  }
  function b() {
    console.log('b: calling c');
    c();
    console.log('b: done');
  }
  function c() {
    console.log('c: throwing error');
    throw new Error('c error');
    console.log('c: done');
  }
  function d() {
    console.log('d: calling c');
    c();
    console.log('d: done');
  }

  try {
    a();
  } catch(err) {
    console.log(err.stack);
  }

  try {
    d();
  } catch(err) {
    console.log(err.stack);
  }

콘솔에 출력해 보면

a: calling b
b: calling c
c: throwing error
Error: c error
// 에러가 발생한 콜 스택

스택을 추적한 결과를 보여주고 가장 깊은 함수에서 시작하고 함수가 남지 않았을 때 끝난다.

try...catch...finally

try 블록의 코드가 HTTP 연결이나 파일 같은 일종의 자원을 처리할 때가 있는데, 프로그램에서 이 자원을 계속 갖고 있을 수 없기 때문에 에러의 유무와 상관없이 이 자원을 해제해야 한다.
finally 블록은 에러가 일어나든, 일어나지 않든 반드시 호출되기 때문에 자원을 해제하는 상황에서 필요하다.

try {
    console.log('this line is executed...');
    throw new Error('whoops!');
    console.log('this line is not...');
  } catch (err) {
    console.log('there was an error...');
  } finally {
    console.log('...always executed');
    console.log('perform cleanup here');
  }

thorw 문을 주석 처리한 후 확인해 보면, finally 블록은 어느 쪽에서든 실행되는 것을 확인할 수 있다.

마무리

프로그래밍을 하면서 에러를 해결하는 과정에서 포기하고 싶은 순간도 많은데,
더 나은 프로그램과 프로그래밍을 위해 항상 예외를 생각하면서 상황에 맞는 에러처리 방법을 통해 에러를 최소한으로 줄이고 에러를 해결해가면서 배워야겠다.