당연히 try ... catch(err)를 사용한다. catch 블럭에 인자로 들어오는 놈은 built-in error 객체인데, 아래와같은 속성을 가졌다.
stack의 경우 name : message at call stack 형식으로 나온다.
try 블럭 안에서도 여러가지 에러가 발생 할 수 있다. 어떤 에러가 일어났는지에 따라 서로 다른 대응이 필요 할 수 있다.
에러 객체는 new 연산자를 통해 생성되는 만큼 instanceof 연산자로 구분 해 줄 수 있다.
let error = new Error(message);
let error = new SyntaxError(message);
let error = new ReferenceError(message);
...
이런 내장 에러 객체 또는 직접 throw로 만들어낸 에러 객체를 구분하여 대응 해 준다. 로깅을 하거나 user에게 메시지를 띄우거나 에러를 유발한 동작을 재시도 하거나...
다만 이때 예상치못한 에러가 발생할 경우를 대비해 해당 에러를 catch 블럭 안에서 다시 throw 해주는 것을 rethrowing이라 한다. 이 에러도 당연히 try ...catch로 받아줘야한다. 한층 바깥에 try ...catch를 써 주는 것이다.
에러 객체는 구체적이어야 좋다. 구체적일수록 디버깅 할 때 덜 헤매기 때문이다. 하지만 모든 에러 핸들링이 디버깅을 위해서만 존재하진 않는다. 그냥 에러의 종류에 따라 어떻게 대응해야하는지만 달라질 수 도 있다. 예를들어 요청한 데이터가 없다는 사실만 알면 충분한 상황에서 무엇때문에 없는지까지는 과한 정보일 수 있다.
아래처럼 몇가지 에러를 묶어(wrapping)하고, 속성에 구체적인 내용을 담아서 시기 적절하게 사용할 수 있다.
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");
}
}
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;
}
}
}
try {
readUser('{bad json}');
} catch (e) {
if (e instanceof ReadError) {
console.log(e);
// Original error: SyntaxError: Unexpected token b in JSON at position 1
console.log("Original error: " + e.cause);
} else {
throw e;
}
}