에러 핸들링

차유림·2021년 12월 2일
1

코어자바스크립트-에러핸들링 글을 정리한 내용입니다.

에러 핸들링을 어떻게 하는 것이 좋은가에 대한 고민을 계속 해왔다.
에러가 발생했을 때 콘솔에만 에러정보를 표시하게 할 경우,
사용자는 동작이 되고있는건지, 무엇이 잘못되었는지 알지 못한다.
따라서 발생한 에러에 따라, 사용자에게 적절한 안내를 해줘야 한다.

만약, 에러를 발생해야 하는 경우에는
단순히 throw new Error("어떤 에러 발생")를 사용해왔는데
Error 클래스를 확장하여 사용하는 방법이 있어 정리해보려고 한다.
커스텀 에러 클래스를 만들면 obj instanceof Error를 사용해
에러 객체를 식별할 수 있다는 장점이 있다.

instancof 연산자
object instanceof constructor
object 의 프로토타입 체인에 constructor.prototype이 존재하는지 판별한다.

Error 객체

mdn에서 Error 객체를 검색해보면 message, name
인스턴스 프로퍼티를 갖고 있음을 알 수 있다.

  • message 프로퍼티 : Error 생성자 함수에 인수로 전달한 에러 메세지
  • stack 프로퍼티 : 에러를 발생시킨 콜 스택의 호출 정보를 나타내는 문자열, 디버깅 목적으로 사용

Error 생성자 함수

Error 생성자 함수는 에러 객체를 생성한다.
Error 생성자 함수에는 에러메세지를 인수로 전달할 수 있다.

const error = new Error("invalid");

자바스크립트는 일반적인 Error 외에
추가로 6가지의 에러 객체를 생성할 수 있는 Error 생성자함수를 제공한다.
이들이 생성한 에러 객체의 프로토타입은 모두 Error.prototype을 상속받는다.

생성자 함수인스턴스
Error일반적인 에러 객체
SyntaxErrorJS 문법에 맞지 않는 문을 해석할 때 발생하는 에러 객체
ReferenceError참조할 수 없는 식별자를 참조했을 때 발생하는 에러 객체
TypeError피연산자 또는 인수의 데이터 타입이 유효하지 않을 때 발생하는 에러 객체
RangeError숫자 값의 허용 범위를 벗어났을 떄 발생하는 에러 객체
URIErrorencodeURI 또는 decodeURI 함수에 부적절한 인수를 전달했을 때 발생하는 에러 객체
EvalErroreval 함수에서 발생하는 에러 객체

에러 확장하기

Error 클래스 슈도 코드

Error class는 어떻게 생겼을까?

// 자바스크립트 자체 내장 에러 클래스 Error의 '슈도 코드'
class Error {
  constructor(message) {
    this.message = message;
    this.name = "Error"; // (name은 내장 에러 클래스마다 다릅니다.)
    this.stack = <call stack>;  // stack은 표준은 아니지만, 대다수 환경이 지원합니다.
  }
}

Error 를 상속받는 ValidationError 클래스 생성

에러 클래스를 직접 만드는 경우,
message, name, stack 프로퍼티를 지원하는게 좋다고 한다.
만약 HttpError라면 statusCode 프로퍼티에 숫자를 지정할 수도 있다.

class ValidationError extends Error {
  constructor(message) {
    super(message); // (1)
    this.name = "ValidationError"; // (2)
  }
}
    1. super()를 통해 부모생성자를 호출한다.
      message 프로퍼티는 부모생성자에서 설정된다.
    1. 부모 생성자에서 설정되는 name 대신 원하는 값으로 재설정하기 위해
      this.name 을 설정해준다.

에러 name 설정
매번 this.name 을 설정해주는 것이 아니라
class 명을 this.name 으로 설정하려면
다음과 같이 MyError 클래스를 만들어
this.constructor.name을 사용하도록 할 수 있다.

class MyError extends Error {
  constructor(message) {
    super(message);
    this.name = this.constructor.name;
  }
}

class ValidationError extends MyError { }

class PropertyRequiredError extends ValidationError {
  constructor(property) {
    super("No property: " + property);
    this.property = property;
  }
}

// 제대로 된 이름이 출력됩니다.
alert( new PropertyRequiredError("field").name ); // PropertyRequiredError

메세지도 설정

메세지도 클래스 안에 지정해주면
사용할 때 메세지 전달없이 호출할 수 있다.

class NotArrayError extends Error {
  constructor() {
    super('배열이 아닙니다.');
    this.name = 'NotArrayError';
  }
}

// 사용
if (!Array.isArray(arr)) {
    throw new NotArrayError();
  }

아래와 같이 에러를 발생시켜보면
에러객체 정보를 확인해 볼 수 있다.

function test() {
  throw new ValidationError("에러 발생!");
}

try {
  test();
} catch(err) {
  alert(err.message); // 에러 발생!
  alert(err.name); // ValidationError
  alert(err.stack); // 각 행 번호가 있는 중첩된 호출들의 목록
}

실제 코드에서는 에러 객체의
instanceof를 사용해 따로 처리를 해줄 수 있다.
상속 클래스에서도 동작하도록 instanceof를 사용하는게 좋지만
서드파티 라이브러리를 사용할 경우, 에러객체 클래스를 알아내는 것이 쉽지 않으므로
err.name 프로퍼티를 사용해 확인할 수도 있다.

function readUser(json) {
  let user = JSON.parse(json);

  if (!user.age) {
    throw new ValidationError("No field: age");
  }
  if (!user.name) {
    throw new ValidationError("No field: name");
  }

  return user;
}

// 에러처리
try {
  let user = readUser('{ "age": 25 }');
} catch (err) {
  if (err instanceof ValidationError) { // 에러 유형 확인
    alert("Invalid data: " + err.message); // Invalid data: No field: name
  } else if (err instanceof SyntaxError) { 
    alert("JSON Syntax Error: " + err.message);
  } else {
    throw err; // 알려지지 않은 에러는 재던지기
  }
}

예외 감싸기

위의 코드에서 resdUser 기능이 커지면
에러 종류가 많아질텐데, 그때마다 catch문에서
에러처리 분기문을 매번 추가하는 것은 번거롭다.

try {
  ...
  readUser()  // 잠재적 에러 발생처
  ...
} catch (err) {
  if (err instanceof ValidationError) {
    // validation 에러 처리
  } else if (err instanceof SyntaxError) {
    // 문법 에러 처리
  } else {
    throw err; // 알 수 없는 에러는 다시 던지기 함
  }
}

중요한것은 "데이터를 읽을때" 에러가 발생했는지 여부이므로
이를 대변하는 새로운 클래스 ReadError를 만들고
구체적인 에러는 readUser 내부에서 잡고,
이때 ReadError를 생성하여 던져줌으로써
readUser를 호출하는 코드에서는 ReadError만 확인할 수 있도록 한다!

ReadErrorcause 프로퍼티에 실제 에러에 대한 참조를 저장하면
추가정보가 필요할 경우 확인할 수 있다.

class ReadError extends Error {
  constructor(message, cause) {
    super(message);
    this.cause = cause;
    this.name = 'ReadError';
  }
}

class ValidationError extends Error { /*...*/ }
class PropertyRequiredError extends ValidationError { /* ... */ }

function validateUser(user) {
  if (!user.age) {
    throw new PropertyRequiredError("age");
  }

  if (!user.name) {
    throw new PropertyRequiredError("name");
  }
}

// readUser 함수 내부에서 발생한 에러는 
// ReadError로 감싸서 던진다
function readUser(json) {
  let user;

  try {
    user = JSON.parse(json);
  } catch (err) {
    if (err instanceof SyntaxError) {
      throw new ReadError("Syntax Error", err);
    } else {
      throw err;
    }
  }

  try {
    validateUser(user);
  } catch (err) {
    if (err instanceof ValidationError) {
      throw new ReadError("Validation Error", err);
    } else {
      throw err;
    }
  }

}

// readUser를 호출할 때 발생한 에러는
// 분기문 처리 없이 ReadError만 확인하면 된다.
try {
  readUser('{잘못된 형식의 json}');
} catch (e) {
  if (e instanceof ReadError) {
    alert(e);
    // Original error: SyntaxError: Unexpected token b in JSON at position 1
    alert("Original error: " + e.cause);
  } else {
    throw e;
  }
}

즉, 모든 에러를 포함하는 추상 에러를 하나 만들고,
에러가 발생하면 추상에러를 던지도록 한다.
추상 에러의 cause 프로퍼티에
실제 발생한 에러를 담으면
구체적인 에러정보를 넘겨줄 수 있다.

참고

profile
🎨프론트엔드 개발자💻

0개의 댓글