본문으로 바로 가기
로고
개발

[JS] 자바스크립트 예외처리 하기

읽는 시간 13분
자바스크립트 예외처리 하기 글의 썸네일"

#들어가며

기계가 아닌이상 프로그래밍을 하다보면 에러가 있는 코드를 작성할 수 있다.(무조건) 그게 개발자의 실수이든 사용하는 쪽의 실수든 서버의 문제든 다양한 이유가 있을 것이다. 예외 처리를 하지않으면 에러 발생 시 프로그래밍이 죽는다. 그럼 우리 프로그램을 죽이지(?) 않도록 예외 처리에 대해 알아보자.

#예외란?

예외처리의 필요성

  • 프로그램의 안정성 강화: 예외처리는 예상치 못한 상황이나 오류가 발생했을 때, 프로그램이 갑자기 중단되는 것을 방지하고 이를 통해 프로그램의 안정성을 높일 수 있음
  • 사용자 경험 향상: 사용자에게 의미 있는 오류 메시지를 제공함으로써, 사용자가 프로그램을 더 잘 이해하고, 필요한 조치를 취할 수 있음
  • 개발자에게 유용한 디버깅 정보 제공: 오류 발생 시, 그 원인을 파악할 수 있는 정보를 개발자에게 제공, 이는 문제 해결과 코드 품질 개선에 도움을 줌

예외의 예시

  • 문법적 오류(Syntax Error): 코드가 자바스크립트의 문법을 따르지 않을 때 발생
  • 타입 오류(Type Error): 잘못된 타입의 값을 사용하려 할 때 발생, 예) 숫자가 아닌 값을 숫자로 간주하려 할 때
  • 참조 오류(Reference Error): 존재하지 않는 변수를 참조하려 할 때 발생

#사용법

#Try, Catch, Finally

자바스크립트에서 예외를 처리하는 데 사용되는 세 가지 기본 구문은 try, catch, finally다. 각 구문의 역할과 사용 방법을 살펴보자.

  1. try 블록

예외가 발생할 수 있는 코드를 감싸는 데 사용된다. try 블록 안의 코드가 실행되는 동안 예외가 발생하면, 즉시 해당 블록의 실행을 중단하고 catch 블록으로 제어를 이동시킨다.

js
try {
    // 예외가 발생할 수 있는 코드
    const result = 에러발생함수();
}
js
try {
    // 예외가 발생할 수 있는 코드
    const result = 에러발생함수();
}
  1. catch 블록

try 블록에서 예외가 발생했을 때 이를 처리하는 코드를 작성하는 곳이다. catch 블록은 발생한 예외에 대한 정보를 담은 오류 객체를 받을 수 있다.

js
catch (error) {
    // 오류 처리 코드
    console.log('에러 발생:', error.message);
}
 
// 굳이 error 객체에 대한 정보가 필요없다면 아래처럼 (error)을 생략하고 사용할 수 있다.
catch { // <-- (error) 생략
 
}
 
js
catch (error) {
    // 오류 처리 코드
    console.log('에러 발생:', error.message);
}
 
// 굳이 error 객체에 대한 정보가 필요없다면 아래처럼 (error)을 생략하고 사용할 수 있다.
catch { // <-- (error) 생략
 
}
 
  1. finally 블록

예외 발생 여부에 상관없이 실행되어야 하는 코드를 작성하는 곳, 이 블록은 주로 리소스를 정리하거나 필수적인 마무리 작업을 수행하는 데 사용된다.

js
finally {
    // 항상 실행되는 코드
    console.log('이 콘솔은 무조건 출력됨');
}
 
js
finally {
    // 항상 실행되는 코드
    console.log('이 콘솔은 무조건 출력됨');
}
 

Q. finally 를 쓰지 않고 똑같이 할 수 있지 않나요?

위 두 가지 코드는 똑같이 작업을 하고, 에러 처리를 하고, 초기화를 하는 동작을 한다. 하지만 finally를 쓴 코드의 이점은 뭘까? 만약 try-catch 구문을 빠져나가게 하는 코드가 있다면 동작이 달라진다.

js
// 1
function withFinally() {
    try {
      throw new Error("에러 발생");
    } catch (error) {
      console.error("error:", error.message);
      return false; // catch 구문에서 return
    } finally {
      console.log("finally가 있다면 실행 된다.");
    }
}
 
// 2
function withoutFinally() {
    try {
      throw new Error("에러 발생");
    } catch (error) {
      console.error("error:", error.message);
      return false; // catch 구문에서 return
    }
 
    // console.log("실행 안됨");
}
js
// 1
function withFinally() {
    try {
      throw new Error("에러 발생");
    } catch (error) {
      console.error("error:", error.message);
      return false; // catch 구문에서 return
    } finally {
      console.log("finally가 있다면 실행 된다.");
    }
}
 
// 2
function withoutFinally() {
    try {
      throw new Error("에러 발생");
    } catch (error) {
      console.error("error:", error.message);
      return false; // catch 구문에서 return
    }
 
    // console.log("실행 안됨");
}

만약에 위 코드에서 finally가 없었다면 초기화를 에러 발생 시에는 초기화가 되지 않는 버그가 생길 수 있다.

#자바스크립트에서 제공하는 내장 에러 타입

자바스크립트는 다양한 종류의 내장 에러 타입들을 제공합니다. 이들 각각은 특정한 종류의 오류 상황을 나타내며, 이를 이해하는 것은 효과적인 예외 처리와 디버깅에 매우 중요하다. 주요 에러 타입들과 그 특징, 발생 시나리오에 대해 알아보자.

  1. Error (기본 에러): 모든 에러 타입의 기본이 되는 객체, 직접 인스턴스를 생성할 수도 있고, 대부분의 다른 에러 타입들은 이 Error 객체를 상속함
js
throw new Error("기본 에러 메세지");
js
throw new Error("기본 에러 메세지");
  1. TypeError: 변수나 매개변수가 예상된 타입이 아닐 때 발생, 일반적으로 잘못된 타입의 값이 함수에 전달되었을 때 발생
js
let num = 5;
num.toUpperCase(); // TypeError: 'num.toUpperCase is not a function' 에러 발생
js
let num = 5;
num.toUpperCase(); // TypeError: 'num.toUpperCase is not a function' 에러 발생
  1. ReferenceError: 존재하지 않는 변수를 참조하려고 할 때 발생, 변수의 스코프 문제나, 아직 선언되지 않은 변수를 참조하려 할 때 발생
js
console.log(undeclaredVariable); // ReferenceError: 'undeclaredVariable is not defined' 에러 발생
js
console.log(undeclaredVariable); // ReferenceError: 'undeclaredVariable is not defined' 에러 발생

세 가지 모든 경우에서 공통적으로 Uncaught 를 볼 수 있는데, 에러를 던졌지만 그 오류를 잡아내고 처리하는 catch 블록이 없다는 것을 의미한다.

#사용자 정의 예외 만들기

자바스크립트에서는 기본 제공되는 에러 타입 외에도 사용자가 직접 정의한 예외 타입을 만들어 사용할 수 있다. 이를 통해 특정한 오류 상황에 맞춘 더 상세하고 명확한 에러 처리를 할 수 있다.

Error 클래스를 상속받아 사용자 정의 예외 클래스를 만든다. 이렇게 하면 기본적인 에러 처리 기능을 유지하면서 추가 정보를 포함시킬 수 있다. Error 객체의 기본정보에는 message, name, stack 등 이 있다.

js
throw new Error("에러 메세지") // Uncaught Error: 에러 메세지 at <anonymous>:1:7
js
throw new Error("에러 메세지") // Uncaught Error: 에러 메세지 at <anonymous>:1:7

위 에러 메세지에서 name은 "Error"고 message는 "에러 메세지", 그리고 at 부분이 에러 스택 부분을 의미한다.

Custom Error

js
class MyCustomError extends Error { // Error가 아니라 TypeError, ReferenceError를 상속받을 수 도 있다.
    constructor(message) {
        super(message); // 부모 클래스의 생성자 호출
        this.name = "MyCustomError"; // 에러 이름 설정
        this.message = `${message} 에러가 발생했습니다.`
    }
}
js
class MyCustomError extends Error { // Error가 아니라 TypeError, ReferenceError를 상속받을 수 도 있다.
    constructor(message) {
        super(message); // 부모 클래스의 생성자 호출
        this.name = "MyCustomError"; // 에러 이름 설정
        this.message = `${message} 에러가 발생했습니다.`
    }
}

커스텀 에러는 위 코드처럼 Error 클래스를 상속받아서 우리만의 커스텀 에러 클래스를 만들 수 있다. 커스텀 에러를 throw하면 catch 하는 부분에서 아래처럼 처리할 수 있다.

js
function someFunction() {
    // 특정 조건에서 사용자 정의 예외 발생
    if (/* some condition */) {
        throw new MyCustomError("Something went wrong");
    }
}
 
try {
    someFunction();
} catch (error) {
    if (error instanceof MyCustomError) {
        console.log(error.name, error.message); // 사용자 정의 에러 처리
    } else {
        console.log("Other error:", error.message);
    }
}
 
js
function someFunction() {
    // 특정 조건에서 사용자 정의 예외 발생
    if (/* some condition */) {
        throw new MyCustomError("Something went wrong");
    }
}
 
try {
    someFunction();
} catch (error) {
    if (error instanceof MyCustomError) {
        console.log(error.name, error.message); // 사용자 정의 에러 처리
    } else {
        console.log("Other error:", error.message);
    }
}
 

Custom Error를 사용하면 에러가 어떤 에러인지 내가 정해준 name을 통해, 코드 및 에러 로그에서 명확하게 알 수 있기 때문에 가독성이 올라가고, 에러 처리를 더 세밀하게 할 수 있다는 장점이 있다.

#비동기 코드에서의 예외처리

지금까지는 동기코드에서의 예외처리만 알아보았는데, 비동기 코드에서 예외처리를 올바르게 하기 위해서는 몇 가지 주의해야 할 점이 있다.

  • 프로미스의 catch 메소드 사용
    • 프로미스 체인의 마지막에 .catch() 메소드를 사용하여 비동기 작업 중 발생하는 예외를 잡아낸다.
    • .then() 메소드에서 발생하는 에러도 .catch()에서 처리될 수 있다.
js
fetchData()
  .then(data => processData(data))
  .catch(error => console.error("에러 발생:", error));
js
fetchData()
  .then(data => processData(data))
  .catch(error => console.error("에러 발생:", error));
  • async/awaittry/catch
    • async 함수 내에서 await 키워드를 사용한 비동기 작업은 try/catch 블록으로 감싸져 예외를 처리할 수 있다.
    • 이 방법을 이용하면 동기 코드와 유사한 구조로 예외 처리를 할 수 있다.
js
async function AsyncAwait() {
  try {
    const data = await fetchData();
  } catch (error) {
    console.error("에러 발생:", error);
  }
}
js
async function AsyncAwait() {
  try {
    const data = await fetchData();
  } catch (error) {
    console.error("에러 발생:", error);
  }
}

#비동기 코드에서 예외처리 시 주의사항

비동기 함수 내에서 발생하는 예외는 try-catch 로 잡을 수 없다.

js
try {
    setTimeout(function() {
        throw new Error("에러 발생!");
    }, 1000);
} catch (error) {
    console.error("에러가 잡힐까?:", error.message);
}
 
// Uncaught Error: 에러 발생!
js
try {
    setTimeout(function() {
        throw new Error("에러 발생!");
    }, 1000);
} catch (error) {
    console.error("에러가 잡힐까?:", error.message);
}
 
// Uncaught Error: 에러 발생!

비동기인 setTimeout 이 실행될 시점이 아니라 1초 후에 에러를 던지기 때문에 catch에 잡히지 않는다. 이건 이벤트 루프와 비동기 실행 메커니즘의 특징 때문이다. 아무튼 setTimeout 내부의 function 안에서 try-catch를 하던가 프로미스를 리턴하는 함수를 만들어서 .catch() 에서 처리 혹은 .then() 의 두 번째 인자(onRejected)로 처리해 줄 수 있다.

#마치며

이렇게 자바스크립트에서의 예외처리를 알아봤는데, 뭔가 쓰고보니 간단한 얘기밖에 없는데 길게 쓰게 된 것 같다.. 그래도 알고 넘어가야 할 부분들 이라고 생각한다! 나중에는 제너레이터를 이용한 예외처리 처럼 좀 더 심화된 내용을 공부해서 소개해보고싶다.

#참고자료

javascript.info

코드종 유튜브