개발을 하다 보면 자체 에러 클래스가 필요한 경우가 생길 수 있다. 네트워크 관련 작업 중 에러가 발생했다면 HttpError, 데이터베이스 관련 작업 중 에러가 발생했다면 DbError, 검색 관련 작업 중 에러가 발생했다면 NotFoundError를 사용하는 것이 직관적이기 때문이다.
앞서 배운 바와 같이 throw의 인수엔 아무런 제약이 없기 때문에 커스텀 에러 클래스는 반드시 Error를 상속할 필요가 없다. 그렇지만 Error를 상속받아 커스텀 에러 클래스를 만들게 되면 obj instanceof Error를 사용해서 에러 객체를 식별할 수 있다는 장점이 생긴다. 이런 장점 때문에 맨땅에서 커스텀 에러 객체를 만드는 것보다 Error를 상속받아 에러 객체를 만드는 것이 낫다고 한다.
-> 애플리케이션 크기가 점점 커지면 우리가 만들게 될 커스텀 에러 클래스들은 자연스레 계층 구조를 형성하게 되니까 더 편해진다.
에러를 정의하려면 에러를 상속받아야 하는데 그 전에 Error 클래스의 기본적인 구조를 알아야 한다.
// 자바스크립트 자체 내장 에러 클래스 Error의 '슈도 코드'
class Error {
constructor(message) {
this.message = message; // 에러 메시지
this.name = "Error"; // 에러로 표시될 이름
this.stack = [call stack]; // stack은 표준은 아니지만, 대다수 환경이 지원함
}
}
그렇다면 이제 직접 에러를 생성해보자!
class ValidationError extends Error {
constructor(message) {
super(message); // (1)
this.name = "ValidationError"; // (2)
}
}
function test() {
throw new ValidationError("에러 발생!");
}
try {
test();
} catch(err) {
alert(err.message); // 에러 발생!
alert(err.name); // ValidationError
alert(err.stack); // 각 행 번호가 있는 중첩된 호출들의 목록
}
Error객체 역시 Array처럼 클래스로 상속받을 수 있다. 나는 값 검증 실패 에러를 만들기 위해 에러 이름을 ValidationError로 지정한 에러 클래스 ValidationError를 선언했다.
(1)에서 부모 생성자를 호출하고 있다는 것에 주목하자. 자바스크립트에서는 자식 생성자 안에서 super를 반드시 호출해야 한다. message 프로퍼티는 부모 생성자에서 설정되기 때문이다.
부모 생성자에선 message뿐만 아니라 name 프로퍼티도 설정("Error")하기 때문에, (2)에서 원하는 값으로 재설정해주었다.
아래의 사진은 alert(err.stack);을 했을 때 어떻게 나오는 지 궁금해서 확인해봤다.
다음과 같은 에러 출력을 얻을 수 있었다. name프로퍼티로 설정한 에러의 이름과 객체를 생성할 때 넣어준 message값, 에러가 발생하기 까지의 call stack이 출력되는 것을 알 수 있다.
방금 구현한 ValidationError클래스가 너무 포괄적이서 뭔가 잘못될 확률이 있다. 그럴때에는 한번 더 상속해 새로운 에러를 만들 수 도 있다. 이번에는 데이터를 포함한 구체적인 에러 클래스를 생성해보자.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
class PropertyRequiredError extends ValidationError {
constructor(property) {
super("No property: " + property);
this.name = "PropertyRequiredError";
this.property = property;
}
}
// 사용법
function readUser(json) {
let user = JSON.parse(json);
if (!user.age) {
throw new PropertyRequiredError("age");
}
if (!user.name) {
throw new PropertyRequiredError("name");
}
return user;
}
// try..catch와 readUser를 함께 사용하면 다음과 같다.
// 에러 유형 확인은 instanceof
try {
let user = readUser('{ "age": 25 }');
} catch (err) {
if (err instanceof ValidationError) {
alert("Invalid data: " + err.message); // Invalid data: No property: name
alert(err.name); // PropertyRequiredError
alert(err.property); // name
} else if (err instanceof SyntaxError) {
alert("JSON Syntax Error: " + err.message);
} else {
throw err; // 알려지지 않은 에러는 재던지기 한다.
}
}
다음과 같이 ValidationError를 상속받은 PropertyRequiredError클래스를 작성했다. PropertyRequiredError클래스는 객체에 특정 프로퍼티가 없을 때 발생시키는 에러이다. 이전 에러 클래스와 다른 점은 property가 추가되었다는 점이다.
에러 출력을 확인해보니 이전과는 달리 property라는 값이 출력되는 것을 알 수 있다. 이런 식으로 커스텀 에러를 작성 하면 에러 출력을 보고 어떤 에러이고, 어떤 프로퍼티에서 문제가 생겼는지 빠르게 파악할 수 있다.
라고 했지만, 굳이 ValidationError를 상속받은 것이 이해가 잘 안갔는데 🎀서히🎀가 작성해준 밑의 코드를 보고 이해했다!
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
class PropertyRequiredError extends ValidationError {
constructor(property) {
super("No property: " + property);
this.name = "PropertyRequiredError";
this.property = property;
}
}
// 사용법
function readUser(json) {
let user = JSON.parse(json);
console.log(user);
if (!user.age && user.name) {
throw new PropertyRequiredError("age");
} else if (user.age && !user.name) {
throw new PropertyRequiredError("name");
} else {
throw new ValidationError("❗️ Validation ❗️");
}
return user;
}
function whatError(data) {
try {
let user = readUser(data);
} catch (err) {
if (err instanceof PropertyRequiredError) {
console.log("Invalid data: " + err.message);
console.log(err.name); // PropertyRequiredError
console.log(err.property); // name
} else if (err instanceof ValidationError) {
console.log("Invalid data: " + err.message);
console.log(err.name);
console.log(err.property);
} else if (err instanceof SyntaxError) {
console.log("JSON Syntax Error: " + err.message);
} else {
throw err; // 알려지지 않은 에러는 재던지기 한다.
}
}
}
whatError('{ "age": 25 }');
/*{ age: 25 }
Invalid data: No property: name
PropertyRequiredError
name */
console.log("-----------");
whatError("{ }");
/*{}
Invalid data: ❗️ Validation ❗️
ValidationError
undefined */
맨 위에서 말했듯이 상속을 하면 에러 클래스들은 자연스레 계층 구조를 형성한다.
때문에 에러 발생 시, (1) 어느 클래스 에러인지 (2) 무엇때문에 에러가 발생했는지 한 번에 파악할 수 있기에 상속을 사용하는 것 같다!!
에러 계층 구조가 커질 수록 더 다양한 에러를 한번에 파악할 수 있다.