본 포스팅은 여기에 올라온 게시글을 바탕으로 작성되었습니다.
파트와 카테고리 동일한 순서로 모든 내용을 소개하는 것이 아닌, 몰랐거나 새로운 내용 위주로 다시 정리하여 개인공부 목적으로 작성합니다.
중간중간 개인 판단 하에 필요하다고 생각될 시, 기존 내용에 추가로 보충되는 내용이 있을 수 있습니다.
프로그래밍을 하다보면 필연적으로 에러가 발생하기 마련이다. 그 원인은 실수, 예상치 못한 사용자 입력, 네트워크 오류 등 수만가지에 달할 수 있다.
에러가 발생하면 자바스크립트는 즉시 중단되고 콘솔에 에러가 출력된다. 이때 try...catch
문법을 사용하면 자바스크립트가 즉시 중단되는 것을 방지하고, 에러를 잡아서 관련된 처리를 수행할 수 있다.
try...catch
문법은 다음과 같이 크게 두 개의 블록으로 구성된다.
try {
// 정상 실행
} catch (err) {
// 에러 발생 시 처리
}
해당 구문의 내부 순서 흐름은 다음과 같다.
try {...}
안의 코드 실행try
안의 마지막 줄까지 실행 후 catch
블럭은 무시try
안의 코드 실행이 중지되고, catch(err)
블록으로 흐름이 넘어감. 매개변수 err
(아무 이름이나 가능)엔 무슨 일이 일어났는지에 대한 정보를 포함하고 있는 객체가 포함try...catch
는 오직 런타임 에러에만 정상 동작한다. 즉 실행 가능하지 않은 자바스크립트 코드에는 반응하지 않는다. 애초에 유효하지 않은 자바스크립트 코드는 엔진에 의해 parse-time
과정에서 에러가 발생하는데, 엔진인 이 코드 자체를 이해할 수 없기 때문에 try...catch
로는 에러를 잡을 수가 없다. 런타임 에러는 에외(exception)이라고 부르기도 한다.
try {
{{; // parse-time Error - 동작하기 전에 에러 감지 -> 복구 불가
} catch (err) {
console.log('Something Wrond!');
}
또한 try...catch
는 동기적으로 동작한다. 다음과 같이 비동기요청이 일어나는 경우의 에러는 의도한 바와 다르게 작동한다. 앞서 살펴보았듯이 setTimeout
은 try...catch
블록을 떠나고 나서 실행되기 때문이다.
try {
setTimeout(function() {
somethingWrong; // 스크립트는 여기서 죽음
}, 1000); // 그러나 이는 trycatch 블록 이후에 실행되기에
} catch (err) { // catch 블록에서 잡아낼 수 없음
console.log(err.message);
}
위와 같이 스케쥴링 된 함수 또는 비동기 요청 내부의 예외를 잡으려면 해당 함수 내부에 try...catch
를 구현해야 한다.
setTimeout(function() {
try {
somethingWrong;
} cathc (err) {
console.log(err.message);
}
}, 1000);
에러가 발생하면 자바스크립트는 에러 상세내용이 담겨있는 객체를 생성하고, 이를 catch
블럭에 인수로 전달한다. 내장 에러 전체와 에러 객체는 두 가지 주요 프로퍼티를 가진다.
name
: 에러 이름. ReferenceError
, SyntaxError
, TypeError
등 다양하다.message
: 에러 상세 내용을 담고 있는 문자열 메시지.stack
: 표준은 아니지만, 현재 호출 스택. 에러를 유발한 중첩 호출들의 순서 정보를 가진 문자열로 주로 디버깅 목적으로 사용. 대부분의 호스트환경에서 지원.비교적 최근에 스펙에 추가된 문법이다. 따라서 구식 브라우저에서는 폴리필이 필요할 수 있다. 대단한 문법은 아니고, 만약 에러를 취급할 때 굳이 err
객체의 자세한 정보가 필요하지 않다면 이를 생략할 수 있는 문법이다.
try {
// ...
} catch {
// ...
}
앞서 다룬 바 있는 JSON.parse
메서드를 통해 에러를 핸들링 해보자. JSON.parse
는 잘못된 형식의 json
타입이 들어오는 경우 에러를 만들어내기 때문에 스크립트가 죽는다. 이때의 에러를 감지해서 적절한 에러처리를 해준다면 원활한 스크립트의 흐름을 만들 수 있을뿐 아니라 사용자 경험 역시 향상시킬 수 있다.
let json = { 'bad json tpe ' };
try {
let user = JSON.parse(json); // 에러 발생
console.log(user); // 에러로 인해 이 부분은 무시되고 catch 블럭으로 이동
} catch (err) {
console.log(err.name);
... // 적절한 에러처리
}
이 같은 흐름에서 catch
블럭 내에 새로운 네트워크 요청, 사용자에게 대안 제시 또는 로깅 장치에 에러 정보 보내기 등과 같은 구체적인 작업을 수행할 수 있다.
그 밖에 추가적으로 개발자가 직업 에러를 생성하고 알릴 수 있다. 이를 '에러를 던진다'라고 보통 표현하는데 그 이유는 해당 과정에서 throw
연산자를 사용하기 때문이다. 만약 json
이 문법적으로는 잘못되지 않았지만, 스크립트 내에서 필수적으로 사용되는 name
과 같은 프로퍼티가 없을 때는 문법적으로 이를 캐치할 수는 없다. 자바스크립트에서 사용하지 않는 프로퍼티를 참조하는 것은 별다른 에러를 만들지 않기 때문이다. 이런 경우의 에러 역시 처리하고 싶다면 직접 에러를 던져주도록 하자.
throw
연산자는 에러를 생성하는데 문법은 다음과 같다.
throw <Error Obejct>
이론적으로는 숫자, 문자열과 같은 원시형 자료를 포함한 어떤 것이는 에러 객체(Error Object)가 될 수 있다. 하지만 내장 에러와의 호환성을 고려해 되도록 에러 객체는 name
과 message
프로퍼티를 넣어주는 것을 권장한다. 또한 자바스크립트에서 지원하는 표준 에러 객체 관련 생성자를 사용하여 에러를 사용하는 것을 더 추천한다. 이처럼 일반 객체가 아닌 내장 생성자를 이용해 만든 내장 에러 객체의 name
프로퍼티는 생성자 이름과 동일한 값을 가진다.
let error = {
name: 'some error',
message: 'some error message',
};
console.log(error.name); // some error
// 내장 에러 생성자 이용
let error = new Error(message);
console.log(error.name); // Error
let error = new SyntaxError(message);
console.log(error.name); // SyntaxError
let error = new ReferenceError(message);
console.log(error.name); // ReferenceError
잘못된 json
타입을 파싱할 때 발생하는 에러는 SyntaxError
이다. 우리가 사용할 user
객체에는 반드시 name
프로퍼티가 존재해야 한다고 하자. 따라서 name
프로퍼티가 없는 경우는 에러가 발생한 것으로 간주하고 예외 처리를 다음과 같이 해줄 수 있다.
let json = { "age" : 30 }; // name 프로퍼티가 없으므로 에러
try {
let user = JSON.parse(json); // 문법적인 에러는 발생 X
if (!user.name) { // name 프로퍼티가 없으면 에러를 던짐
throw new SyntaxError("name 프로퍼티가 없음");
}
console.log( user.name ); // 실행되지 않음
} catch (err) { // 위에서 던져진 에러는 여기서 캐치
console.log( "JSON Error: " + err.message );
// JSON Error: name 프로퍼티가 없음
}
위 예시에서 정의한 에러 외적으로 또 다른 에러가 발생할 수 있다. 예를 들어 정의되지 않은 변수 사용 등의 프로그래밍 에러가 발생할 수 있는 가능성을 배제할 수 없다. 에러는 어떤 상황에서도 발생할 수 있고, 이러한 에러는 주로 예측 불가한 경우에 많이 생기기 때문이다.
위에서는 '불완전한 데이터(name 프로퍼티가 없는)'를 다루려는 목적으로 try...catch
를 사용했다. 하지만 본래 catch
구문은 try
블록에서 발생한 모든 에러를 감지하기 위해 사용된다. 따라서 만약 try
블록에서 미처 예상하지 못한 에러가 발생하면 catch
블록은 이를 감지하고 잡아낼 수 있지만, 해당 에러는 정의되지 않았을 확률이 높기에 적절하게 처리되지 않을 수 있다.
let json = { 'age' : 30 };
try {
user = JSON.parse(json); // 엄격모드에서 에러 발생 (let 키워드 생략)
...
} catch (err) {
console.log('JSON Error: ' + err.message);
}
위 예시에서 변수 초기화에 let
키워드를 생략했기에 에러가 발생한다. 그러나 이러한 에러 발생 여부를 고려하지 않았기 때문에, catch
블록에서는 감지한 에러를 아까와 동일하게 JSON Error
로 처리한다. 에러가 발생한 부분은 json
파싱부분에서 발생한 것이 아닌데 이와 같은 처리는 추후 디버깅을 어렵게 만들 수 있다.
이러한 문제를 피하기 위해 다시 던지기(re-throwing) 기술을 사용한다. 규칙은 간단하다. catch
는 알고 있는 에러만 처리하고 나머지는 다시 던지는 것이다.
catch
는 모든 에러를 받는다.catch (err) { ... }
블록 내부에서 에러 객체 err
를 분석한다.throw err
를 호출한다.내부에서 에러 객체를 분석할 때는 주로 instanceof
연산자를 사용한다. 에러를 다시 던져서 catch
블록에서는 SyntaxError
만 처리되도록 해보자.
let json = { 'age' : 30 };
try {
let user = JSON.parse(json);
if (!user.name) {
throw new SyntaxError('name 프로퍼티가 없음');
}
somethieWrong(); // 예상치 못한 에러 발생
console.log(user.name);
} catch (err) {
// 발생한 에러가 SyntaxError일 경우에만 처리
if (err instanceof SyntaxError) {
console.log("JSON Error: ", err.message);
} else { // 그렇지 않은 경우엔
throw err; // 에러 다시 던지기
}
}
위에서 다시 던져진 에러는 외부에 try...catch
블럭이 추가로 있다면 해당 영역에서 에러를 다시 잡을 수 있다. 만약 그렇지 않은 경우에는 스크립트는 다시 죽을 수 있다. 이와 같은 기술을 이용해 catch
블럭에서는 어떻게 다룰지 정확하게 알고 있는 에러만 처리하고, 나머지 에러의 경우에는 외부에 위임할 수 있다.
function readData() {
let json = { 'age' : 30 };
try {
...
somethingWrong(); // Error 발생
} catch (err) {
...
if ( !(err instanceof SyntaxError)) {
// 알 수 없는 에러는 다시 던지기
throw err;
}
}
}
try {
readData();
} catch (err) {
// 정의되지 않은 에러는 이 부분에서 처리
console.log('External Error catch: ', err.name);
}
에러 핸들링인 추가적으로 finally
블럭 역시 사용가능하다. finally
블럭 내부는 항상 실행이 보장되는 영역으로 다음과 같은 플로우를 가진다.
try
실행이 끝난 후 실행catch
실행이 끝난 후 실행따라서 finally
블록은 무언가를 실행하고, 실행 결과에 상관없이 실행을 완료하고 싶을 경우 사용된다. finally
블록은 try...catch
절을 빠져나가는 어떤 경우에도 실행되는 것을 보장하는데, 따라서 에러가 발생하는 경우 외에도 return
문 과 같이 명시적으로 빠져나가는 경우에도 finally
블럭은 항상 실행된다.
function func() {
try {
return 1;
} catch (err) {
console.log(err);
} finally {
console.log('finally');
}
}
func(); // finally
또한 굳이 catch
절이 필요하지 않다면 단순히 try...finally
구문도 상황에 따라 유용하게 사용할 수 있다. 주로 내부에서는 별도로 에러를 처리하지 않지만, 시작한 프로세스가 확실하게 마무리 되었는지 확인하는 용도로 사용할 수 있다.
try...catch
밖에서 치명적인 에러가 발생하면 스크립트가 죽게된다. 이때 이를 대처할 수 있는 방안이 없을까? 어딘가에 에러 내역을 로깅의 형태로 기록하거나, 사용자에게 에러가 발생했음을 알려주는 동작을 구현할 수 있을 것이다.
자바스크립트 명세서에는 이러한 치명적인 에러에 대응하는 방식이 등재되어 있지는 않다. 그러나 대부분의 환경에서 try...catch
외부에서 발생한 에러를 잡는 것은 매우 중요한 요소이기 때문에, 호스트환경 대부분은 자체적으로 에러 처리 기능을 제공하고 있다.
Node
의 경우 process.on("uncaughtException")
을 통해 외부에서 발생한 에러를 처리하며, 브라우저 환경의 경우엔 window.onerror
를 이용해 처리할 수 있다. window.onerror
프로퍼티에 함수를 할당하면, 예상치 못한 에러가 발생했을 때 해당 함수가 실행된다.
window.onerror = function(message, url, line, col, error) {
...
};
message
: 에러 메시지url
: 에러가 발생한 스크립트의 URLline, col
: 에러가 발생한 곳의 줄과 열 번호error
: 에러 객체그러나 전역 핸들러 window.onerror
는 죽어버린 스크립트를 복구하려는 목적으로는 잘 사용하지 않는다. 애초에 에러가 발생한 경우에 window.onerror
만으로 복구를 하는것은 사실상 불가능에 가깝기 때문이다. 따라서 window.onerror
는 주로 개발자에게 에러 메시지를 보내는 용도로 사용한다.
이를 응용해서 에러 로깅 메시지를 기록하는 상용 서비스들이 있는데 https://errorception.com/ 또는 https://www.muscula.com/ 과 같은 서비스들이 비교적 유명하다.
자바스크립트에 내장된 애러 객체 외에 별도의 애러 객체를 사용해야 할 경우가 종종 생긴다. 예를 들어 네트워크 관련 에러 발생 시엔 HttpError
, 데이터베이스 관련 작업 중 에러가 발생하면 DBError
, 검색 관련 작업 도중 발생한 에러는 NotFoundError
등을 사용할 수 있을 것이다.
직접 에러 클래스를 만든 경우에는 이 에러들이 message
와 name
등의 프로퍼티를 지원하도록 만들어야 기존 에러 객체들과 호환이 잘 될 것이다. 물론 추가적으로 부가적인 프로퍼티를 더 구현할 수 있다.
앞서 다룬 바와 같이 throw
의 인수에는 아무런 제약이 없기 때문이 굳이 객체 형태가 아닌 원시값 역시 에러로 전달될 수 있다. 그렇지만 기존에 존재하는 Error
를 상속받아 커스텀 에러 클래스를 만들게 되면 obj instanceof Error
를 사용해서 에러 객체를 식별할 수 있다는 장점이 생긴다. 때문에 맨땅에서 커스텀 에러를 만드는 것보단 Error
를 상속받아 에러 객체를 만드는 것이 더 추천된다.
앞서 JSON
객체를 다룰 때 발생할 수 있는 SyntaxError
에 대해 살펴보았다. 이 에러는 변환과정에서 발생하는 문법적인 에러를 캐치하지만, 꼭 필요한 프로퍼티 등이 누락된 경우는 정상적으로 잡아낼 수 없었다.
따라서 JSON
형태의 데이터를 읽을 수 있을 뿐만 아니라, 데이터를 검증할 수 있도록 검증이 필요하다. 이때 데이터 검증에서 발생할 수 있는 에러는 SyntaxError
가 아니다. 자체적인 기준에 맞지 않아 발생하는 에러기 때문에 이와 연관이 없다. 따라서 이를 커스텀 에러로 만들어 관리해보자. 이 에러의 이름을 ValidationError
라고 하자.
ValidationError
클래스엔 문제가 되는 필드 정보가 저장되어야 한다. 내장 클래스 Error
를 상속받아 해당 클래스를 구현하자. 이때 슈도 코드로 Error
클래스를 나타내면 다음과 같다.
class Error {
constructor(messge) {
this.message = message;
this.name = 'Error';
this.stack = <call stack>;
}
}
이를 상속받는 ValidatinoError
는 다음과 같이 구현할 수 있을 것이다. 이때 클래스 챕터에서 다룬바와 같이 생성자 함수에서 super()
를 통해 부모 생성자를 호출한다는 것을 주의하자.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
}
}
우리가 직접 정의한 커스텀 에러를 다음과 같이 사용할 수 있다.
function readUser(json) {
let user = JSON.parse(json);
if (!user.name) {
throw new ValidationError('No field: name');
}
if (!user.age) {
throw new ValidationError('No filed: age');
}
return user;
}
try {
let user = readUser('{ "age" : 30 }');
} catch (err) {
if (err instanceof ValidationError) {
console.log("Invlid: ", err.message);
} else if (err instanceof SyntaxError) {
console.log("JSON Synstax Error: ", err.message);
} else {
// 알려지지 않은 에러이므로 다시 던져서 외부에서 처리 유도
throw err;
}
}
이와 같이 커스텀 에러 클래스 ValidationError
를 사용해 JSON.parse
에서 발생하는 SyntaxError
와 검증 에러를 모두 처리할 수 있게 구현했다.
에러를 구분하는 것은 err.name
을 체크하여 분할할 수도 있다. 각각의 에러 이름은 서로 다르기 때문에 이를 기준으로 구별할 수 있기 때문이다. 그러나 보통은 instanceof
연산자를 통해 구분하는 것이 더 좋다. 추후 상속을 통해 확장 에러 클래스를 만들게 되더라도 instanceof
연산자는 새로운 상속 클래스에도 확장성있게 동작하기 때문이다.
만약 서드 파티 라이브러리에서 온 에러인 경우에는 클래스를 알아내는 것이 쉽지 않을 수 있다. 이러한 경우엔
err.name
으로 분기처리를 할 수 있다.
앞서 만든 ValidationError
클래스는 너무 포괄적이어서 무언가 잘못되거나 놓치는 사항이 발생할 수 있다. 가령 꼭 필요한 프로퍼티가 누락되거나, 문자열 값에 숫자가 들어가는 등의 잘못된 형식이 기입될 수 있다. 따라서 이를 처리할 수 있도록 더욱 구체적은 에러 클래스 PropertyRequiredError
를 만들어보자. 이 클래스는 앞서 구현한 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 {
let user = readUser('{ "age" : 30 }');
} catch (err) {
// PropertyRequiredError는 ValidationError를 상속받기 때문에
// 동일하게 ValidationError의 인스턴스로 취급된다.
if (err instanceof ValidationError) {
console.log("Invalid : ", err.message);
console.log(err.name, err.property);
} else if (err instanceof SyntaxError) {
console.log("JSON Syntax Error : ", err.message);
} else {
throw err;
}
}
기존 ValidationError
를 PropertyRequiredError
로 세분화하면서 조금 더 사용하기 쉽게 바뀌었다. 사람이 읽을 수 있는 message
는 생성자가 알아서 만들어주고, 개발자는 프로퍼티의 이름만 전달해주면 되기 때문이다.
여기서 주목할 점은 PropertyRequiredError
생성자 안에서 this.name
을 수동으로 할당해주고 있다는 점이다. 그런데 이렇게 매번 커스텀 에러 클래스 생성자 안에서 this.name
을 일일이 할당하는 것은 다소 번거롭다. 이러한 작업은 기본 에러 클래스를 만들고 커스텀 에러들이 이 클래스를 상속받게 함으로써 자동화할 수 있다. 기본 에러 생성자에 this.name = this.constructor.name
한 줄을 추가함으로써 이를 구현할 수 있다.
class MyDefaultError extends Error {
constructor(message) {
super(message);
// this는 런타임 환경에서 결정되기 때문에
// 이를 호출한 클래스의 프로퍼티로 name이 지정
this.name = this.constructor.name;
}
}
class ValidationError extends MyDefaultError { }
class PropertyRequiredError extends ValidationError {
constructor(property) {
super('No property: ', property);
this.property = property;
}
함수 readUser
는 사용자 데이터를 읽기위한 용도로 만들어졌다. 그러나 사용자 데이터를 읽는 과정에서 다른 오류가 발생할 수 있는데, 앞으로 해당 함수의 기능과 규모가 더 커지면 또 다른 커스텀 에러를 만들어야 할 수 있다.
따라서 readUser
를 호출하는 곳에선 새롭게 만들어질 커스텀 에러들을 처리할 수 있어야 한다. 그러나 지금은 catch
블록 안에서 단순히 여러 개의 if
문으로 종류를 알 수 있는 에러를 처리하고, 그렇지 않은 에러는 다시 던지기를 통해 처리하고 있다.
이러한 구조에서 미래에 readUser
의 기능이 더 고도화되면서 취급해야 할 에러의 경우도 많아질 텐데, 그때마다 에러 종류에 따른 에러 처리 분기문을 매번 추가하는 것은 역시 번거로운 작업이 될 것이다. 하지만 보통의 경우에서 우리가 필요로 하는 정보는 데이터를 읽을 때 에러가 발생했는지에 대한 여부인 경우가 많다. 왜 에러가 발생했는지, 그에 대한 자세한 이유는 있으면 물론 좋겠지만 대부분의 경우엔 필요로 하지 않는다.
이럴때 사용할 수 있는 기법으로 예외 감싸기(Wrapping Exception)가 있다. 이는 다음과 같은 순서로 진행할 수 있다. 예외 감싸기는 쉽게 말해 에러를 구분하는 기준을 기점으로 하여 관련 공통 로직을 모아놓은 클래스를 만드는 것과 같다. 유사한 예로 공통되는 로직을 모아 만드는 함수와 비슷하다고 봐도 좋다.
ReadError
생성readUser
에서 발생한 ValidationError
, SyntaxError
등의 에러는 readUser
내부에서 잡고 이때 ReadError
를 throw
ReadError
객체의 cause
프로퍼티엔 실제 에러에 대한 참조 저장이와 같이 예외 감싸기 기술을 사용해 스키마를 변경하면 readUser
를 호출하는 코드에서 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');
}
}
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("{ 잘못된 형식의 json }");
} catch (err) {
if (err instanceof ReadError) {
console.log(err, err.cause);
} else {
throw err;
}
}
위와 같이 에러 감싸기를 통해 공통 로직을 통합시켜 준다면, 외부에서는 instanceof ReadError
하나만 확인해도 된다는 장점이 있다. 즉 에러 처리 분기문을 일일이 여러개 구성할 필요가 없다. 이러한 기법은 객체 지향 프로그래밍에서 이미 널리 쓰이고 있는 패턴이다.