javascript

[Javascript] 'try...catch'와 에러 핸들링

sewonzzang123 2022. 1. 11.
반응형

 

'try...catch'와 에러 핸들링

보통의 스크립트에서 에러가 발생하면 스크립트가 죽고(즉시 중단되고), 콘솔에 에러가 출력됩니다.

그러나 try...catch문법을 사용하면 스크립트가 죽는 걸 방지하고, 에러를 잡아서(catch) 더 합당한 무언가를 할 수 있게 됩니다.

 

'try...catch' 문법

 

try{
    // 코드
}catch (err){
    // 에러 핸들링
}

 

try...catch 동작 알고리즘은 다음과 같습니다.

  1. 먼저, try{...} 안의 코드가 실행됩니다.
  2. 에러가 없다면, try 안의 마지막 줄까지 실행되고, catch 블록은 건너뜁니다.
  3. 에러가 있다면, try 안 코드의 실행이 중단되고, catch(err) 블록으로 제어 흐름이 넘어갑니다. 변수 err(아무이름이나 사용 가능)는 무슨 일이 일어났는지에 대한 설명이 담긴 객체를 포함합니다.

-- 이해를 도와주는 알고리즘 그림

이렇게 try {...} 블록 안에서 에러가 발생해도 catch 에서 에러를 처리하기 때문에 스크립트는 죽지 않습니다.

 

에러가 발생하는 예시

try{
    console.log('try start....');   //1
     
    errorTest;                      //2


    console.log('try end....');


} catch(err){
    console.log(err, 'error 발생!'); //3
}

 

!! try...catch 는 오직 런타임 에러에만 동작합니다.

 

try...catch 는 시행 가능한 (runnable) 코드에만 동작합니다. 실행 가능한 코드는 유효한 자바스크립트 코드를 의미 합니다.

중괄호 짝이 안 맞는 것처럼 코드가 문법적으로 잘못된 경우에는 try...catch가 동작하지 않습니다.

 

try{
    {{{{{{{{{
}catch(error){
    console.log('유효하지 않은 코드이기 때문에, catch에 잡히지 않습니다!');
}
// Error
  • parse-time 에러 : 코드를 읽는 도중에 발생하는 에러 , 엔진은 이 코드를 이해할 수 없기 때문에 parse-time 에러는 코드 안에서 복구가 불가능합니다.
  • runtime error (런타임 에러) or Exception (예외) : 유효한 코드 안에서 발생한 에러. try...catch 문은 런타임 에러에만 동작합니다..!

 

!! try...catch 는 동기적으로 동작합니다.

 

setTimeout 처럼 '스케줄 된(scheduled)' 코드에서 발생한 에외는 try...catch 에서 잡아낼 수 없습니다.

setTimeout에 넘겨진 익명 함수는 엔진이 try...catch를 떠난 다음에서야 실행되기 때문입니다.

스케줄 된 내부의 에외를 잡으려면, try...catch를 반드시 함수 내부에 구현해야 합니다.

// Bad case
try{
    setTimeout(function(){
        noSuchVariable; // 스크립트는 여기서 죽습니다.
    }, 1000);
}catch(error){
    console.log('작동 멈춤');
}


// Good case
setTimeout(function(){
    try{
        noSuchVariable; // catch 문으로 갑니다!
    }catch(error){
        console.log('작동 멈춤');
    }
},1000);

 

에러 객체

 

에러가 발생하면 자바스크립트는 에러 상세내용이 담긴 객체를 생성합니다. 그 후, catch 블록에 이 객체를 인수로 전달합니다.

try{
    //...
}catch(err){ // <-- '에러 객체', err 대신 다른 이름으로도 쓸 수 있음.
    //...
}

내장 에러 전체와 에러 객체는 두 가지 주요 프로퍼티를 가집니다.

name

  • 에러 이름, 정의되지 않은 변수 때문에 발생한 에러라면 "ReferenceError"가 이름이 됩니다.
    (ReferenceError)

message

  • 에러 상세 내용을 담고 있는 문자 메시지
  • 표준은 아니지만, name 과 message 이외의 대부분의 호스트 환경에서 지원하는 프로퍼티도 있습니다.
    stack은 가장 널리 사용되는 비표준 프로퍼티 중 하나입니다.
    (aa is not defined)

stack

  • 현재 호출 스택, 에러를 유발한 중첩 호출들의 순서 정보를 가진 문자열로 디버깅 목적으로 사용됩니다.
    (ReferenceError: aa is not defined at ...)

 

선택적 'catch' 바인딩

 

에러에 대한 자세한 정보가 필요하지 않으면, catch 에서 이를 생략할 수 있습니다.

try{
    //...
}catch{ // <-- err 없이 쓸 수 있음
    //...
}

 

'try...catch' 사용하기

try...catch 가 실무에서 어떻게 사용되는지 알아봅시다.

앞서 JSON으로 인코딩된 값을 읽을 수 있도록 해주는 JSON.parse(str) 메서드에 배운 바 있습니다.

이 메서드는 주로 서버 등에서 네트워크를 통해 전달받은 데이터를 디코딩하는 데 사용합니다.

전달받은 데이터에 JSON.parse를 호출하는 식으로 사용됩니다.

 

let json = '{"name":"John", "age":30}'; // 서버로부터 전달받은 데이터


let user = JSON.parse(json); // 전달 받은 문자열을 자바스크립트 객체로 변환


// 문자열 형태로 전달받은 user가 프로퍼티를 가진 객체가 됨
console.log(user.name);  // John
console.log(user.age);   // 30

 

잘못된 json이 들어온 경우, JSON.parse는 에러를 만들기 때문에 스크립트가 죽습니다.

서버에서 전달받은 데이터가 잘못되어 스크립트가 죽는 경우, 사용자는 개발자 콘솔을 열지 않는 이상 절대 원인을 알 수 없습니다.

 

try...catch를 사용해 이를 처리해 봅시다.

 
let json = "{bad json}";


try{
    let user = JSON.parse(json);
    console.log(user.name);
}catch(error){
    console.log('error 가 있어 재요청을 시도합니다.');
    console.log(error.name);
    console.log(error.message);
}

 

이 예시에서는 에러가 발생했다는 것 만을 보여주지만,

catch 블록 안에서 새로운 네트워크 요청 보내기, 사용자에게 대안 제안하기, 로깅 장치에 에러 정보 보내기 등과 같은 구체적인 일을 할 수 있습니다.

 

직접 에러를 만들어서 던지기 'throw' 연산자

throw 연산자는 에러를 생성합니다.

 

이론적으로 object 자리에 어떤 것이든 에러 객체(error object)로 사용할 수 있습니다.

그러나 내장 에러와의 호환을 위해 여러 객체에 name과 message 프로퍼티를 넣어주는 것을 권장합니다.

 

자바스크립트는 Error, SyntaxError, ReferenceError, TypeError 등의 표준 에러 객체 관련 생성자를 지원합니다.

이 생성자들로 여러 에러 객체를 만들 수도 있습니다.

let error = new Error(message);
// or
let error = new SyntaxError(message);
let error = new ReferenceError(message);
// ...

일반 객체가 아닌 내장 생성자를 사용해 만든 내장 에러 객체의 name 프로퍼티는 생성자 이름과 동일한 값을 갖습니다.

프로퍼티 message 의 값은 인수에서 가져옵니다.

 

 

잘못된 데이터를 받았을 때, JSON.parse가 어떤 종류의 에러를 만들어내는지 확인해보았습니다.

SyntaxError 가 발생하게 됩니다.

 

사용자를 나타내는 객체에 name 프로퍼티는 반드시 있어야 하므로, 이제 name이 없으면 에러가 발생하는 것으로 간주하고 예외처리를 만들어 보았습니다.

throw 연산자를 사용해 에러를 던져보겠습니다.

let json = '{"age":30}';


try{
    let user = JSON.parse(json);
    if(!user.name){
        throw new SyntaxError('not found user.name');
    }
}catch(error){
    console.log('error!',error.message);
}


// error! not found user.name

throw 연산자는 message 를 이용해 SyntaxError를 생성합니다.

에러가 발생했으므로 try의 실행은 즉시 중단되고 제어 흐름이 catch로 넘어간 것을 console창으로 확인할 수 있습니다.

 

이제, JSON.parse에서 에러가 발생한 경우를 포함해서 모든 에러를 catch 블록 안에서 처리할 수 있게 되었습니다.

 

에러 다시 던지기 (rethrowing)

위 예시에선 불완전한 데이터를 try...catch로 처리하였습니다.

그런데 또 다른 예기치 않은 에러가 try {...} 블록 안에서 발생 할 수도 있습니다.

 

정의되지 않은 변수 사용 등의 프로그래밍 에러가 발생할 가능성은 항상 있습니다.

let json = '{"age":30}';


try{
    user = JSON.parse(json); // let 이 없으므로 error
}catch(error){
    console.log("JSON Error",error); // JSON Error ReferenceError: user is not undefined
    // 실제론 JSON Error가 아닌데..
}

 

위와 같이 에러 종류와 관계없이 동일한 방식으로 에러를 처리하는 것은 디버깅을 어렵게 만들기 때문에 좋지 않습니다.

이런 문제를 피하고자 '다시 던지기 (rethrowing)' 기술을 사용합니다.

  1. catch가 모든 에러를 받습니다.
  2. catch(err) {...} 블록 안에서 에러 객체 err를 분석합니다.
  3. 에러 처리 방법을 알지 못하면 throw err를 합니다.

보통 에러 타입을 instanceof 명령어로 체크합니다.

에러를 던지는 목표는 에러를 막는 것이 아니라 에러가 발생하면 더욱 편하게 디버깅하는데 있습니다!

 

try...catch...finally

try...catch는 finally 라는 코드 절을 하나 더 가질 수 있습니다.

finally 안의 코드는 다음과 같은 상황에서 실행되빈다.

  • 에러가 없는 경우 : try 실행이 끝난 후
  • 에러가 있는 경우 : catch 실행이 끝난 후

 

finally 를 사용하면 try...catch를 다음과 같이 확장할 수 있습니다.

 

try{
    // ... 코드
}catch(error){
    // ... 에러 핸들링
}finally{
    // 항상 실행
}
try{
    console.log('try');
    if(confirm('error 만들까요?')) error();
}catch(e){
    console.log('catch');
}finally{
    console.log('finally');
}

 위 코드는 두 가지 경로로 실행됩니다.

  • error 만들까요? 에 confirm : try >> catch >> finally
  • error 만들까요? 에 no : try >> finally

finally 절은 무언가를 실행하고, 실행 결과에 상관없이 실행을 완료하고 싶을 경우 사용합니다.

finally 절은 try...catch 절을 빠져나가는 어떤 경우에도 실행됩니다. return을 사용해 명시적으로 빠져나가려는 경우도 마찬가지입니다.

catch 절 없이 try...finally 구문도 상황에 따라 유용하게 쓸 수 있습니다. try...finally 안에선 에러를 처리하고 싶지 않지만, 시작한 프로세스가 마무리 되었는지 확실히 하고 싶은 경우에 사용됩니다.

 


요약

try..catch를 이용하면 런타임 에러를 처리할 수 있습니다. try에선 코드를 실행하고, 에러가 발생하면 catch에서 잡아냅니다.

문법은 다음과 같습니다.

try {
    // 이곳의 코드를 실행
} catch(err) {
    // 에러가 발생하면, 여기부터 실행됨
    // err는 에러 객체
} finally {
    // 에러 발생 여부와 상관없이 try/catch 이후에 실행됨
}

 

try..catch , try..catch...finally이외에도 try...finally를 사용할 수 있습니다.

에러 객체엔 다음과 같은 프로퍼티가 있습니다.

  • message – 사람이 읽을 수 있는 형태의 에러 메시지
  • name – 에러 이름을 담은 문자열 (에러 생성자 이름)
  • stack – 표준이 아니지만 대부분의 호스트 환경이 지원하는 프로퍼티로 에러가 발생한 순간의 스택을 나타냄

에러 객체가 필요 없으면 catch(err) { 대신 catch {를 쓸 수 있습니다.

throw 연산자를 사용하면 에러를 직접 만들 수 있습니다. 이론상으론, throw 인수에 모든 것을 넘길 수 있지만, 대개 내장 Error 클래스를 상속받은 에러 객체를 인수에 넘깁니다.

다시 던지기는 에러 처리 시 사용되는 중요한 패턴입니다. catch 블록에선 대개 예상하였거나 어떻게 다룰지 알고 있는 에러를 다루고, 예상치 못한 에러는 다시 던지기 합니다.

try..catch가 없어도 대부분의 호스트 환경이 ‘전역’ 에러 핸들러를 지원하기 때문에 ‘떨어져 나온’ 에러를 잡을 수 있습니다. window.onerror는 브라우저 환경의 전역 에러 핸들러입니다.

 

 

출처 : https://ko.javascript.info/try-catch

 

'try..catch'와 에러 핸들링

 

ko.javascript.info

 

반응형

댓글