코어자바스크립트-에러핸들링 글을 정리한 내용입니다.
에러 핸들링을 어떻게 하는 것이 좋은가에 대한 고민을 계속 해왔다.
에러가 발생했을 때 콘솔에만 에러정보를 표시하게 할 경우,
사용자는 동작이 되고있는건지, 무엇이 잘못되었는지 알지 못한다.
따라서 발생한 에러에 따라, 사용자에게 적절한 안내를 해줘야 한다.
만약, 에러를 발생해야 하는 경우에는
단순히 throw new Error("어떤 에러 발생")
를 사용해왔는데
Error 클래스를 확장하여 사용하는 방법이 있어 정리해보려고 한다.
커스텀 에러 클래스를 만들면 obj instanceof Error
를 사용해
에러 객체를 식별할 수 있다는 장점이 있다.
instancof 연산자
object instanceof constructor
object 의 프로토타입 체인에constructor.prototype
이 존재하는지 판별한다.
mdn에서 Error 객체를 검색해보면 message, name
등
인스턴스 프로퍼티를 갖고 있음을 알 수 있다.
Error 생성자 함수는 에러 객체를 생성한다.
Error 생성자 함수에는 에러메세지를 인수로 전달할 수 있다.
const error = new Error("invalid");
자바스크립트는 일반적인 Error 외에
추가로 6가지의 에러 객체를 생성할 수 있는 Error 생성자함수를 제공한다.
이들이 생성한 에러 객체의 프로토타입은 모두 Error.prototype을 상속받는다.
생성자 함수 | 인스턴스 |
---|---|
Error | 일반적인 에러 객체 |
SyntaxError | JS 문법에 맞지 않는 문을 해석할 때 발생하는 에러 객체 |
ReferenceError | 참조할 수 없는 식별자를 참조했을 때 발생하는 에러 객체 |
TypeError | 피연산자 또는 인수의 데이터 타입이 유효하지 않을 때 발생하는 에러 객체 |
RangeError | 숫자 값의 허용 범위를 벗어났을 떄 발생하는 에러 객체 |
URIError | encodeURI 또는 decodeURI 함수에 부적절한 인수를 전달했을 때 발생하는 에러 객체 |
EvalError | eval 함수에서 발생하는 에러 객체 |
Error class는 어떻게 생겼을까?
// 자바스크립트 자체 내장 에러 클래스 Error의 '슈도 코드'
class Error {
constructor(message) {
this.message = message;
this.name = "Error"; // (name은 내장 에러 클래스마다 다릅니다.)
this.stack = <call stack>; // stack은 표준은 아니지만, 대다수 환경이 지원합니다.
}
}
에러 클래스를 직접 만드는 경우,
message
, name
, stack
프로퍼티를 지원하는게 좋다고 한다.
만약 HttpError
라면 statusCode
프로퍼티에 숫자를 지정할 수도 있다.
class ValidationError extends Error {
constructor(message) {
super(message); // (1)
this.name = "ValidationError"; // (2)
}
}
message
프로퍼티는 부모생성자에서 설정된다.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
만 확인할 수 있도록 한다!
ReadError
에 cause
프로퍼티에 실제 에러에 대한 참조를 저장하면
추가정보가 필요할 경우 확인할 수 있다.
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
프로퍼티에
실제 발생한 에러를 담으면
구체적인 에러정보를 넘겨줄 수 있다.