본문 바로가기

테스트

동기, 비동기에서의 예외처리

요약

Node.js 개발 환경에서 에러 핸들링 방법에는 (1) Throw, (2) Try & Catch, (3) Middleware 이 있음

Throw와 Try-Catch를 이용한 방식은 각각 동기, 비동기를 기준으로 문법을 달리 표현이 가능

Middleware를 이용한 방식은 다양한 에러를 개발자가 의도한 특정 에러로 수렴하여 전달할 수 있다는 장점이 있음

비동기 에러 핸들링을 Middleware를 이용하여 처리하려면 async-wrap 과 같은 별도의 컨트롤러를 적용해야 함

각각의 에러 핸들링 방식은 특정 한계점이 있으며, 서로 상호 보완하는 모습이 있음

 

 

예외 (Exception)

사전적 의미의 예외는 일반적인 통례나 정해진 규칙에서 벗어남 을 의미한다.

그렇다면 프로그래밍에서 예외란 무엇일까? 

자바스크립트를 쓰는 상황에서는 자바스크립트 문법에 벗어나는 상황이 발생했을 경우를 의미할 것이다.

즉 예외란 처리하지 못한 에러

(에러는 노드 스레드를 멈춤, 그러므로 싱글 스레드인 노드에서 스레드가 멈춘다는 것은 프로세스가 멈추는 것이여서
에러 처리는 필수!)

코드로 보면 아래와 같은 상황에 대응되는 Error가 발생한다.

이 에러는 자바스크립트 내부에서 정의됨

fun(;
//틀린 문법을 사용해서 발생한 SyntaxError;

obj.x;
//선언하지 않은 obj.x를 실행함에 따라 발생한 ReferenceError

 

 

 

위의 에러와는 다르게

아래는 우리가 코드상으로 의도하지 않은 대로 사용할 경우 자바스크립트 내부에서 정의된 에러가 아닌

내가 코드상으로 정의한 예외 처리를 통해서 잘못사용하고 있다는 것을 표현할 수 있음

이런 잘못 사용한 상황이 발생할 경우 예외를 발생시킬 수 있음

 

 

function sum(x,y) {
    if (typeof x !== 'number' || typeof y !== 'number' ) {
        throw '숫자가 아닙니다.'
    }    
    return x+y;
}

console.log(sum(1,2));
//3

console.log(sum('a',2));
//숫자가 아닙니다.

 

 

 

 

 

function f2 () {
    console.log('f2 시작')
    console.log('f2 끝')
}

function f1 () {
    console.log('f1 시작')
    f2();
    console.log('f1 끝')
}

console.log('will : f1');
f1();
console.log('did : f1');

 

 

 

 

위 코드에서 f2쪽에 throw를 선언하면

function f2 () {
    console.log('f2 시작')
    throw '에러';
    console.log('f2 끝')
}

function f1 () {
    console.log('f1 시작')
    f2();
    console.log('f1 끝')
}

console.log('will : f1');
f1();
console.log('did : f1');

 

 

f2가 실행될 때, throw를 만나 f2 콜스택이 없어지고

f1 콜스택 안에 있는 f2 정보가 없어지니 f1 콜스택도 없어지고

결국 글로벌 콜스택도 없어져서 실행된 코드 중간에서 멈춰짐

 

 

에러가 발생할 곳을 try catch로 감싸면

에러가 발생하면 코드가 중간에 멈춰지는 것이 아니라 아래와 같이 throw로 정의한 에러가 출력되면서

이후 코드가 실행됨 

function f2 () {
    console.log('f2 시작')
    throw '에러';
    console.log('f2 끝')
}

function f1 () {
    console.log('f1 시작')
    try {
        f2();
    } catch (error) {
        console.log(error)
    }
    console.log('f1 끝')
}

console.log('will : f1');
f1();
console.log('did : f1');

 

 

 

try catch 문을 사용하는 위치가 달라지면 어떻게 될까?

에러가 발생한 이후의 f1() 코드는 실행되지 않고

f1() 호출 된 그 다음이 바로 실행됨

function f2 () {
    console.log('f2 시작')
    throw '에러';
    console.log('f2 끝')
}

function f1 () {
    console.log('f1 시작')
    f2();
    console.log('f1 끝')
}

console.log('will : f1');

try {
    f1();
} catch (error) {
    console.log(error)
}

console.log('did : f1');

 

 

 

Error 객체

throw 이후 에러 메세지를 아래와 같이 Error 객체를 새로 생성시켜서 선언하면 어떻게 될까?

에러 객체를 사용해서 메세지를 넣으면

에러 객체에는 해당하는 콜스택 정보가 담겨 있음

 

function f2 () {
    console.log('f2 시작')
    throw new Error ('에러');
    console.log('f2 끝')
}

function f1 () {
    console.log('f1 시작')
    f2();
    console.log('f1 끝')
}

console.log('will : f1');

try {
    f1();
} catch (error) {
    console.log(error)
}

console.log('did : f1');

 

왼쪽 오른쪽 같은 코드의 출력 내용임

에러 객체에는

어떤 파일의 몇번째 줄, 어떤 함수와 같은 콜스택 정보가 출력됨

즉 실행되고 있는 위치에서의 콜스택 정보를 알 수 있음

 

근데 비동기 일 때는 예외 처리를 다르게 해줘야함!

 

Promise와 catch에서의 예외처리

먼저 Promise 객체는 해야하는 일을 했을 때, 성공했냐 실패했냐 의 결과가 담긴 객체!

function wait (sec) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log("전")
            reject('error!');
            console.log("후")
        }, sec*1000);
    })
}

wait(3);

 

wait 함수를 실행 시키면

아래와 같이 에러가 처리되지 않은 예외인 UncaughtException이 발생

 

 

wait 실행 중 UncaughtException이 발생했으니 try catch 문으로 감쌌지만

동일한 에러가 발생!

function wait (sec) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log("전")
            reject('error!');
            console.log("후")
        }, sec*1000);
    })
}

//추가!!!!!!
try {
    wait(3);    
} catch (error) {
    console.log(error)
}

 

그 이유는 예외를 발생하는 타이밍이 try catch 문안에 있는 wait 메소드 코드가 실행되는 타이밍이 아님

 wait를 실행시키기 위한 콜스택은 실행이 되어 없어지고

이 안에 비동기적으로 실행되는 setTimeout 메소드는 큐에 넣어지고 그 큐에 내용이 새로운 콜 스택을 생성하기 때문에

위의 try catch 구문 안에서 예외가 발생되지 않음

 

 

그래서 promise의 예외처리는 동기적일 때와 다르게 처리해야함!

function wait (sec) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log("전")
            reject('error!');
            console.log("후")
        }, sec*1000);
    })
}

wait(3).catch(e => {
    console.log(e)
})

console.log('끝')

 

 

wait 메소드 의 return은 Promise

보통 체인은 계속 같은 객체를 리턴함

그래서 wait 실행 뒤 return 된 Promise 객체를 첫번째 catch에서 받아서 처리하고 이전과는 다른 Promise 객체 (이 Promise는 캐치 자체 행동이 제대로 됐는지에 해당하는 Promise)를 return하고 이를 두번째 catch에서 받아서 Promise 객체를 리턴

그래서 아래 예시에서 wait로 인한 Promise가 첫번째 catch에서 처리가 끝나서 두번째 catch에서는 동작하지 않음 

function wait (sec) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log("전")
            reject('error!');
            console.log("후")
        }, sec*1000);
    })
}
wait(3)
    .catch(e => {
        console.log('1st catch', e)
    })
    .catch(e => {
        console.log('2nd catch', e)
    })
console.log('끝')

 

 

 

두번째 catch가 동작하게 하려면 아래와 같이

첫번째 catch에서 처리할거 처리하고 두번째 catch에서 첫번째의 throw e를 잡아서 예외 처리를 함

 

function wait (sec) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log("전")
            reject('error!');
            console.log("후")
        }, sec*1000);
    })
}
wait(3)
    .catch(e => {
        console.log('1st catch', e)
        throw e;
    })
    .catch(e => {
        console.log('2nd catch', e)
    })
console.log('끝')

 

catch를 계속 쓰는 방법 말고도 then을 이용해서도 할 수 있음

function wait (sec) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log("전")
            reject('error!');
            console.log("후")
        }, sec*1000);
    })
}
wait(3)
    .then(
        () => {
            console.log('done!!!!')
        },
        e => {
        console.log('1st catch in Then', e)
        throw new Error('throw in Then')
    })
    .catch(e => {
        console.log('2nd catch', e)
    })
console.log('끝')

 

 

 

 

 

 

 

async/await의 예외

 

 

 

 

 

 

 

 

참고

https://velog.io/@yenicall/Node.js%EC%97%90%EC%84%9C%EC%9D%98-Error-Handling-%EC%A0%81%EC%9A%A9

 

Node.js에서의 Error Handling 적용

그림1 throw를 이용한 에러 흐름도에러를 던지는 방법으로 throw 가 있다. 이는 개발자가 작성하는 모듈에서 발생가능한 에러 상황에서 던지게 되며 상위 계층이나 호출하는 곳에서 모듈의 에러를

velog.io