[javascript] 'try..catch'와 에러 핸들링

skdus·2022년 5월 15일
1

JavaScript

목록 보기
9/17
post-thumbnail

아무리 프로그래밍에 능한 개발자라도 에러가 있는 코드를 작성할 수 있다. 개발자가 아무리 완벽한 코드를 작성해도 실수가 있을 수 있고 여러 외부 요인(사용자의 잘못 된 값 입력, 데이터 통신 오류 등)이 있기 때문에 항상 개발자는 작성한 코드가 에러로 인해 중단되지 않도록 여러 해결책을 만들어야 한다.

에러가 발생하면 스크립트는 ‘죽고’(즉시 중단되고), 콘솔에 에러가 출력된다.

이런 모든 에러를 예측하고 if/else문을 사용해 예외 처리를 할 수도 있지만 개발자가 모든 에러를 예측하기는 쉽지 않다. 그리고 가독성 또한 낮아진다.

그렇기에 try...catch문을 활용해 코드가 중단되는 것을 방지하고 에러에 대한 예외처리 등을 작성하면 좀 더 안정적인 코드를 작성할 수 있다.

💡 try...catch

말 그대로 try(시도)해서 에러가 생기면 catch(잡는) 문법이다. 그렇기 때문에 try와 catch라는 두 개의 주요 블록으로 구성된다.

try {
	// 실행될 코드
} catch (error) {
	// 에러 발생 시 실행할 코드
}

try...catch 동작 알고리즘은 다음과 같다.

  1. 먼저 try{...} 안의 코드가 실행된다.
  2. 에러가 없다면, try안의 마지막 줄까지 실행되고, catch블록은 건너 뛴다.
  3. 에러가 있다면, try안 코드의 실행이 중단되고, catch(error)블록으로 제어 흐름이 넘어가며 변수 error는 무슨 일이 일어났는지에 대한 정보가 담긴 에러 객체를 포함한다.

이렇게 try{...}블록 안에서 에러가 발생하더라도 catch문으로 흐름이 넘어가며 에러를 처리하기 때문에 실행중인 코드가 중단되지 않는다.

그렇다면 try...catch는 모든 에러를 처리할까? 아니다. try...catch는 모든 에러를 처리해주는 만능 에러 핸들러는 아니다. 몇가지 예외가 있다.


💡 try...catch는 오직 런타임 에러에만 동작한다.

try...catch는 실행 가능한(runnable) 코드에서만 동작한다. 즉, 문법적으로 유효한 자바스크립트 코드에서만 동작한다.

try {
	// parse-time error
	{/=234..
} catch (error) {
	alert("유효하지 않은 코드");
}

자바스크립트 엔진은 코드를 모두 읽고 난 후 코드를 실행한다. 코드를 읽는 도중 발생한 에러는 'parse-time 에러'라고 부르는데, 엔진은 이 코드를 이해할 수 없기 때문에 parse-time 에러는 코드 안에서 복구가 불가능하다.

try...catch는 유효한 코드에서 발생하는 에러만 처리할 수 있다. 이런 에러를 '런타임 에러(runtime error)' 혹은 '예외(exception)'라고 부른다.


💡 try...catch는 동기적으로 동작한다.

setTimeout이나 Promise처럼 비동기적으로 동작하는 코드에서 발생한 에러는 try...catch에서 잡아낼 수 없다.

try {
  setTimeout(() => {
    noSuchVariable; //스크립트 죽음
  }, 1000);
} catch (error) {
  console.error('에러 발생!');
}

setTimeout에 넘겨진 함수는 엔진이 try...catch문을 벗어난 뒤 실행되기 때문에 비동기적으로 동작하는 코드 내부에 try...catch문을 구현해야 한다.

// 에러 발생!
setTimeout(() => {
  try {
    noSuchVariable;
  } catch (error) {
    console.error('에러 발생!');
  }
}, 1000);

💡 에러 객체

try블록에서 에러가 발생하면 자바스크립트는 에러에 대한 데이터를 담은 객체를 생성한다. 그리고 catch 블록에 이 객체를 인수로 전달한다.

try {
	// 실행될 코드
} catch (error) { // <- 생성된 에러 객체, eer 대신 다른 이름으로도 선언 가능
	// 에러 발생 시 실행할 코드
}

내장 에러 전체와 에러 객체는 두 가지 주요 프로퍼티를 가진다.

  • name: 에러 이름, 정의도지 않은 변수 때문에 발생한 에러라면 ReferenceError가 이름이 된다.
  • message: 에러 상세 내용을 담고 있는 문자 메시지
    표준은 아니지만 name과 message이외에 대부분의 호스트 환경에서 지원하는 프로퍼티도 있다. stack은 가장 널리 사용되는 비 표준 프로퍼티 중 하나로 현재 호출 스택, 에러를 유발한 중첩 호출들의 순서 정보를 가진 문자열이다. 보통 디버깅 목적으로 사용된다.
try {
  lalala; // 에러, 변수가 정의되지 않음!
} catch(err) {
  alert(err.name); // ReferenceError
  alert(err.message); // lalala is not defined
  alert(err.stack); // ReferenceError: lalala is not defined at ... (호출 스택)

  // 에러 전체를 보여줄 수도 있습니다.
  // 이때, 에러 객체는 "name: message" 형태의 문자열로 변환됩니다.
  alert(err); // ReferenceError: lalala is not defined
}

🌱 선택적 catch 바인딩
에러에 대한 자세한 정보가 필요하지 않다면 catch에서 이를 생략할 수 있다.

try {
	// 실행될 코드
} catch { // <-- (err) 없이 쓸 수 있음
	// 에러 발생 시 실행할 코드
}

💡 직접 에러를 만들어 던지기

json이 문법적으로 잘못되진 않았지만, 스크립트 내에서 사용 중인 필수 프로퍼티 name을 가지고 있지 않다면 무슨 일이 생길까?

const json = '{ "age": "24" }';

try {
  const user = JSON.parse(json); //전달받은 문자열을 자바스크립트 객체로 변환
  console.log(user.name); //error 발생
} catch (error) {
  console.error('error');
}

다음 코드에서 JSON.parse는 정상적으로 작동 하지만 user객체에 name 프로퍼티가 없기 때문에 개발자가 의도한 상황이 아니다. 이런 경우 에러를 발생해야 하지만 자바스크립트에서는 undefined를 출력해 에러가 발생하지 않는다.

이럴 때에는 throw연산자를 사용해 직접 에러를 생성할 수 도 있다.

throw <error object>

문법은 다음과 같다. 이론적으로는 숫자, 문자열과 같은 원시형 자료를 포함한 어떤 것이든 에러 객체로 사용할 수 있지만 내장 에러와의 호환을 위해 되도록 에러 객체에 name과 message프로퍼티를 넣어주는 것을 권장한다.

자바스크립트는 Error, SyntaxError, ReferenceError, TypeError등의 표준 에러 객체 관련 생성자를 지원한다. 이 생성자들을 이용해 아래와 같이 에러 객체를 만들 수 있다.

const error = new Error('message');
const syntaxError = new SyntaxError('message');
const referenceError = new ReferenceError('message');

console.log(referenceError.name); // ReferenceError
console.log(referenceError.message); // 'message'

일반 객체가 아닌 내장 생성자를 사용해 만든 내장 에러 객체의 name 프로퍼티는 생성자 이름과 동일한 값을 갖으며 인자로 넣어준 문자열은 message프로퍼티가 갖는다.

다음 코드는 user.name이 없는지 확인해서 throw연산자를 사용해 인자로 넣어준 메시지를 가진 에러를 생성해줬다.

const json = '{ "age": "24" }';

try {
  const user = JSON.parse(json);
  if (!user.name) {
    throw new SyntaxError('해당 프로퍼티가 존재하지 않습니다.');
  }
  console.log(AvenJS);
} catch (error) {
  // 해당 프로퍼티가 존재하지 않습니다.
  console.error(error.message);
}

throw 연산자는 message를 이용해 SyntaxError를 생성한다. 에러 생성 방식은 자바스크립트가 자체적으로 에러를 생성하는 방식과 동일하다. 그래서 에러가 발생했으므로 try의 실행은 즉시 중단되고 제어 흐름이 catch로 넘어간 것을 확인할 수 있다.


💡 에러 다시 던지기

위 예시에선 불완전한 데이터를 try..catch로 처리했다. 그런데 또 다른 예기치 않은 에러가 try {...} 블록 안에서 발생 할 수도 있다.

catch는 원래 try 블록에서 발생한 모든 에러를 잡으려는 목적으로 만들어졌다. 그런데 위 예시에서 catch는 예상치 못한 에러를 잡아내 주긴 했지만, 에러 종류와 관계없이 "JSON Error" 메시지를 보여줍니다. 이렇게 에러 종류와 관계없이 동일한 방식으로 에러를 처리하는 것은 디버깅을 어렵게 만들기 때문에 좋지 않다.

이런 문제를 피하고자 ‘다시 던지기(rethrowing)’ 기술을 사용한다. 규칙은 간단한데,
catch는 알고 있는 에러만 처리하고 나머지는 ‘다시 던져야’ 한다.

  1. catch가 모든 에러를 받는다.
  2. catch(err) {...} 블록 안에서 에러 객체 err를 분석한다.
  3. 에러 처리 방법을 알지 못하면, throw err를 한다.
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 ); // 에러를 잡음
}

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

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


💡 try...catch...finally

에러 핸들링은 try...catch뿐만 아니라 finally라는 코드 블럭을 하나 더 가질 수 있다. finally는 try 실행이 끝난 후나, catch 실행이 끝난 후에 실행 된다.

try {
  // 실행될 코드
} catch (error) {
  // 에러 발생 시 실행할 코드
} finally {
  // try블럭 또는 catch블럭 실행 후 무조건 실행
}

try...catch...finally 동작 알고리즘은 다음과 같다.

  1. 먼저 try{...}안의 코드가 실행된다.
  2. 에러가 없다면, try안의 마지막 줄까지 실행되고, catch블록은 건너 뛴뒤 finally블록이 실행된다.
  3. 에러가 있다면, try안 코드의 실행이 중단되고, catch(error)블록으로 제어 흐름이 넘어가며 변수 error는 무슨 일이 일어났는지에 대한 정보가 담긴 에러 객체를 반환한뒤 finally블록이 실행된다.
    finally절은 무언가를 실행하고, 실행 결과에 상관 없이 실행을 완료하고 싶을 경우 사용한다. try나 catch문에서 return으로 강제로 블록을 종료시킨다고 해도 finally는 실행된다.

finally는 catch절이 없는 try...finally도 유용하게 사용할 수 있다. 에러를 처리하고 싶지 않지만, 시작한 작업이 마무리 되었는지 확실히 하고 싶은 경우에 사용한다.

🌱 try..catch..finally 안의 변수는 지역 변수이다.

💡 요약

  • try..catch를 이용하면 런타임 에러를 처리할 수 있다.
  • 에러가 발생해서 스크립트가 죽는 것을 방지하고, 그 error로 다른 무언가를 할 수 있다.
  • throw 연산자를 사용하면 에러를 직접 만들 수도 있다.
  • catch 블록에선 대개 예상하였거나 어떻게 다룰지 알고 있는 에러를 다루고, 예상치 못한 에러는 다시 던지기를 하자.

0개의 댓글