1. 'try..catch'와 에러 핸들링
2. 커스텀 에러와 확장
- 'try..catch' 에러 문법
// 에러가 없는 코드 try { alert('try 블록 시작'); // (1) <-- // ...에러가 없습니다. alert('try 블록 끝'); // (2) <-- } catch(err) { alert('에러가 없으므로, catch는 무시됩니다.'); // (3) } // 에러가 있는 코드 try { alert('try 블록 시작'); // (1) <-- lalala; // 에러, 변수가 정의되지 않음! alert('try 블록 끝(절대 도달하지 않음)'); // (2) } catch(err) { alert(`에러가 발생했습니다!`); // (3) <-- }
- 에러가 없으면
(1) -> (2)
- 에러가 있다면
(1) -> (3)
- 에러가 없으면
[!WARNING]
try..ctch
는 오직 런타임 에러만 동작합니다. 문법적으로 잘못된 경우에는try..catch
가 작동하지 않음try { {{{{{{{{{{{ } catch(e) { alert('유효하지 않은 코드이기 때문에, 자바스크립트 엔진은 이 코드를 이해할 수 없습니다.'); }
자바스크립트 엔진은 코드를 읽고 난 후 코드를 실행함. 코드를 읽는 중에 발생하는 에러는 'parse-time'에러라고 부르는데, 엔진은 이 코드를 이해할 수 없기 때문에 parse-time 에러는 코드 안에서 복구가 불가능함 =>
try..catch
는 유효한 코드에서 발생하는 에러만 처리할 수 있음. 이런 에러를 '런타임 에러' 혹은 '예외'라고 부름
[!WARNING]
try..catch
는 동기적으로 동작합니다. setTimeout처럼 '스케줄 된(scheduled)' 코드에서 발생한 예외는try..catch
에서 잡아낼 수 없음try { setTimeout(function() { noSuchVariable; // 스크립트는 여기서 죽습니다. }, 1000); } catch (e) { alert('작동 멈춤'); }
setTimeout
에 넘겨진 익명 함수는 엔진이try..catch
를 떠난 다음에서야 실행되기 때문 스케줄 된 함수 내부의 예외를 잡으려면,try..catch
를 함수 내부에 구현해야 함setTimeout(function() { try { noSuchVariable; // 이제 try..catch에서 에러를 핸들링함 } catch { alert('에러를 잡았습니다'); } }, 1000);
- 에러 객체의 프로퍼티
name
- 에러 이름 ex) 정의되지 않은 변수 때문에 발생한 에러라면ReferenceError
가 이름이 됨message
- 에러 상세 내용을 담고 있는 문자 메세지stack
- 현재 호출 스택. 에러를 유발한 중첩 호출들의 순서 정보를 가진 문자열로 디버깅 목적으로 사용
try { lalala; // 에러, 변수가 정의되지 않음! } catch(err) { alert(err.name); // ReferenceError alert(err.message); // lalala is not defined alert(err.stack); // ReferenceError: lalala is not defined at ... (호출 스택) // 에러 전체를 보여줄 수도 있습니다. // 이때, 에러 객체는 "name: message" 형태의 문자열로 변환됩니다. alert(err); // ReferenceError: lalala is not defined }
- 에러 정보가 필요 없다면 생략할 수도 있음
try { // ... } catch { // <-- (err) 없이 쓸 수 있음 // ... }
try..catch
를 통해 스크립트가 죽은 원인을 파악할 수 있음catch
블록 안에서 새로운 네트워크 요청 보내기, 사용자에게 대안 제안하기, 로깅 장치에서 에러 정보 보내기 등의 작업을 할 수 있음
let json = "{ bad json }"; try { let user = JSON.parse(json); // <-- 여기서 에러가 발생하므로 alert(user.name); // 이 코드는 동작하지 않음 } catch (e) { // 에러가 발생하면 제어 흐름이 catch 문으로 �넘어옴 alert("데이터에 에러가 있어 재요청을 시도합니다."); alert(e.name); alert(e.message); }
json
이 문법적으로 잘못되지 않고, 필수 프로퍼티를 가지고 있지 않아서 오류가 발생했을 경우throw
연산자를 사용해 에러처리를 진행해야 함throw <error object>
let json = '{ "age": 30 }'; // 불완전한 데이터 try { let user = JSON.parse(json); // <-- 에러 없음 alert(user.name); // 이름이 없습니다! } catch (e) { alert("실행되지 않습니다."); }
- 자바스크립트는
Error
,SyntaxError
,ReferenceError
,TypeError
등의 표준 에러 객체 관련 생성자를 지원하기 때문에 이를 활용해서 에러 객체를 만들 수 있음let error = new Error(message); // or let error = new SyntaxError(message); let error = new ReferenceError(message); // ... // `name` 프로퍼티는 생성자 이름과 동일한 값을 가짐 let error = new Error("이상한 일이 발생했습니다. o_O"); alert(error.name); // Error alert(error.message); // 이상한 일이 발생했습니다. o_O
throw
문을 활용해 에러 객체로 던지기let json = '{ "age": 30 }'; // 불완전한 데이터 try { let user = JSON.parse(json); // <-- 에러 없음 if (!user.name) { throw new SyntaxError("불완전한 데이터: 이름 없음"); // (*) } alert( user.name ); } catch(e) { alert("JSON Error: " + e.message); // JSON Error: 불완전한 데이터: 이름 없음 }
(*)
-throw
연산자는message
를 이용해SyntaxError
를 생성
- 또 다른 예기치 않은 에러가
try {...}
블록 안에 발생할 경우 에러가 발생할 수도 있음 - 정의되지 않은 변수 사용 등의 프로그래밍 에러가 발생할 가능성은 항상 존재
let json = '{ "age": 30 }'; // 불완전한 데이터 try { user = JSON.parse(json); // <-- user 앞에 let을 붙이는 걸 잊었네요. // ... } catch(err) { alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined // (실제론 JSON Error가 아닙니다.) }
- 위의 코드에서는 예상치 못한 에러를 잡아내 주긴 했지만, 에러 종류와 관계 없이
'JSON Error'
메세지를 보여줌 - 이렇게 에러 종류와 관계 없이 동일한 방식으로 에러를 처리하는 것은 디버깅을 어렵게 만듦 => '다시 던지기(rethrowing)' 기술 사용
- 위의 코드에서는 예상치 못한 에러를 잡아내 주긴 했지만, 에러 종류와 관계 없이
- catch는 알고 있는 에러만 처리하고 나머지는 '다시 던져야'함
- catch가 모든 에러를 받음
catch(err) {...}
블록 안에서 에러 객체err
를 분석- 에러 처리 방법을 알지 못하면
throw err
를 함
- 이때의 에러 타입을
instanceof
명령어로 체크try { user = { /*...*/ }; } catch(err) { if (err instanceof ReferenceError) { alert('ReferenceError'); // 정의되지 않은 변수에 접근하여 'ReferenceError' 발생 } }
err.name
프로퍼티로 에러 클래스 이름을 알 수 있음- 기본형 에러는 모두
err.name
프로퍼티를 가짐 orerr.constructor.name
을 사용할 수 있음let json = '{ "age": 30 }'; // 불완전한 데이터 try { let user = JSON.parse(json); if (!user.name) { throw new SyntaxError("불완전한 데이터: 이름 없음"); } blabla(); // 예상치 못한 에러 alert(user.name); } catch(e) { if (e instanceof SyntaxError) { alert("JSON Error: " + e.message); } else { throw e; // 에러 다시 던지기 (*) } }
(*)
- 다시 던져진 에러는try..catch
'밖으로 던져짐'- 바깥에
try..catch
가 있다면 여기서 에러를 잡음. 아니면 스크립트가 죽음
- 바깥에
catch
블록에선 어떻게 다룰지 알고 있는 에러만 처리하고, 알 수 없는 에러는 '건너뛸 수' 있음
- 던져진 에러를 다시
try..catch
문을 통해 에러 검출function readData() { let json = '{ "age": 30 }'; try { // ... blabla(); // 에러! } catch (e) { // ... if (!(e instanceof SyntaxError)) { throw e; // 알 수 없는 에러 다시 던지기 } } } try { readData(); } catch (e) { alert("External catch got: " + e); // 에러를 잡음 }
readData
는SyntaxError
만 처리할 수 있지만, 함수 바깥의try..catch
에서는 예상치 못한 에러도 처리할 수 있게 되었음
finally
를 통해try..catch
를 확장할 수 있음try { alert( 'try 블록 시작' ); if (confirm('에러를 만드시겠습니까?')) 이상한_코드(); } catch (e) { alert( 'catch' ); } finally { alert( 'finally' ); }
- '에러를 만드시겠습니까?'에 'OK'로 답한 경우:
try -> catch -> finally
- 'No'로 답한 경우:
try -> finally
- '에러를 만드시겠습니까?'에 'OK'로 답한 경우:
finally
절은 무언가를 실행하고, 실행 결과에 상관없이 실행을 완료하고 싶을 때 사용- ex) 피보나치 함수
fn(n)
연산 시간을 측정하고 싶을때, 도중에 에러가 발생하면 측정 불가능하니finally
절에 넣음
- ex) 피보나치 함수
[!NOTE]
try..catch..finally
안의 변수는 지역변수입니다.try
블록 내부에서 선언한 변수는 블록 안에서만 유효한 지역변수가 됨
[!NOTE]
finally
와return
finally
절은try..catch
절을 빠져나가는 어떤 경우에도 실행return
을 사용해 명시적으로 빠져나가려는 경우도 마찬가지 아래의 코드의try
블록 안엔return
이 존재하고, 이 경우엔 값이 바깥 코드로 반환되기 전에finally
가 실행됨function func() { try { return 1; } catch(e) { / * ... * / } finally { alert('finally'); } } alert(func()); // finally 안의 alert가 실행되고 난 후, 실행됨
[!NOTE]
try..finally
catch
절이 없는try..finally
구문도 상황에 따라 유용하게 쓸 수 있음try..finally
안에선 에러를 처리하고 싶지 않지만, 시작한 프로세스가 마무리되었는지 확실히 하고 싶은 경우에 사용function func() { try { // ... } finally { // 스크립트가 죽도라도 완료됨 } }
- 자체 에러 클래스를 위한 커스텀 에러를 만들 수 있임
- 직관적임 ex) 네트워크 에러 -
HttpError
/ 데이터베이스 에러 -DbError
/ 검색 에러 -NotFoundError
throw
인수엔 아무런 제약이 없기 때문에 보통Error
를 상속받아서 에러 객체를 생성함 > 상속 받은Error
를 이용해 커스텀 에러를 만듦
- 사용자 데이터가 저장된 JSON을 읽는 함수
readUser(json)
이 존재- 유효한
json
은 아래와 같은 형태let json = `{ "name": "John", "age": 30 }`;
- 유효한
- 잘못된
json
형식이 들어오면SyntaxError
가 발생 - JSON 형식은 맞지만, 프로퍼티가 누락되는 등의 상황이 발생할 경우
- 데이터를 '검증'하는 에러를 추가 >
ValidationError
- 데이터를 '검증'하는 에러를 추가 >
- 일반적으로
Error
클래스를 상복받아ValidationError
클래스로 확장함- 슈도 코드란? 실제 동작 방식을 이해하기 쉽게 설명하기 위해 작성된 가상의 코드 / 구체적인 구현 세부 사항보다는 기본적인 개념을 전달하는 데 초첨이 맞춰있음
// 자바스크립트 자체 내장 에러 클래스 Error의 '슈도 코드' class Error { constructor(message) { this.message = message; this.name = "Error"; // (name은 내장 에러 클래스마다 다릅니다.) 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); // 각 행 번호가 있는 중첩된 호출들의 목록 }
- 자바스크립트에서는 자식 생성자 안에서
super
를 반드시 호출해야 함.message
프로퍼티는 부모 생성자에서 설정됨 - 부모 생성자에선
message
뿐만이 아니라name
프로퍼티도 설정하기 때문에(2)
에서 원하는 값으로 재설정해 주었음
readUser(json)
내부에서 사용하는ValidationError
class ValidationError extends Error { constructor(message) { super(message); this.name = "ValidationError"; } } // 사용법 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..catch와 readUser를 함께 사용한 예시 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; // 알려지지 않은 에러는 재던지기 합니다. (**) } }
try..catch
블록에서 커스텀 에러ValidationError
와JSON.parse
에서 발생하는SyntaxError
둘 다 처리할 수 있게 되었음instanceof
를 통해 에러 유형 확인 가능((*)
)err.name
으로도 에러 유형 확인 가능// ... // (err instanceof SyntaxError) 대신 사용 가능 } else if (err.name == "SyntaxError") { // (*) // ...
- 위의 코드에서 에러 유형은
instanceof
를 사용하는 것을 권장((*)
) - 나중에
ValidationError
를 확장해서PropertyRequiredError
같은 새로운 확장 에러를 만들게 될 때,instanceof
는 새로운 상속 클래스에서도 동작함 catch
블록에선 유효성 검사와 문법 오류만 처리하고, 다른 종류의 에러는 밖으로 던져야 함((**)
)
-
ValidationError
는 프로퍼티가 누락되거나age
에 문자열 값이 들어가는 것처럼 형식이 잘못된 경우를 처리할 수 없음 -
필수 프로퍼티가 없는 경우에 대응할 수 있도록 구체적인 에러를 만듦 >
PropertyRequiredError
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를 함께 사용하면 다음과 같습니다. 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; // 알려지지 않은 에러는 재던지기 합니다. } }
this.property
에 누락된 속성의 이름을 저장하는 프로퍼티를 전달하면 됨
-
PropertyRequiredError
도ValidationError
처럼this.name
을 수동으로 할당해 주었음 > 매번 커스텀 에러 클래스의 생성자에 할당해 주는 것은 번거로움 > '기본 에러' 클래스를 만들고 커스텀 에러들이 이 클래스를 상속받게 하면 해결 가능 -
기본 에러의 생성자에
this.name = 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
- 현재는
ValidationError
,SyntaxError
로만 에러 처리를 진행하고 있지만readUser
가 커질수록 에러를 확장할 필요가 있음 - 하지만 에러 종류에 따라 에러 처리 분기문을 매번 추가해야하는 불편함이 존재 > '예외 감싸기'를 통해 예방 가능
- '데이터 읽기'와 같은 포괄적인 에러를 대변하는 새로운 클래스
ReadError
를 만듦 - 함수
readUser
발생한ValidationError
,SyntaxError
등의 에러는readUser
내부에서 잡고, 이때ReadUser
를 생성함 ReadUser
객체의cause
프로퍼티엔 실제 에러에 대한 참조가 저장됨
- '데이터 읽기'와 같은 포괄적인 에러를 대변하는 새로운 클래스
- 예외 감싸기 기술을 통해 스키마를 변경하면
readUser
를 호출하는 코드에선ReadError
만 확인하면 됨- 에러 종류 전체를 확인할 필요 X / 추가 정보가 필요한 경우엔
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 (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; } }
- Syntax 에러나 Validation 에러가 발생한 경우 해당 에러 차제를 다시 던지기하지 않고
ReadError
를 던지게 됨 readUser
를 호출하는 바깥 코드에선instanceof ReadError
하나만 확인하면 됨e.cause
를 통해 구체적인 에러 정보를 확인할 수 있지만 필수 프로퍼티는 아님
- 에러 종류 전체를 확인할 필요 X / 추가 정보가 필요한 경우엔