자바스크립트 Promise에서 에러핸들링시 주의점
자바스크립트는 개인적으로 가장 오래 써온 언어중 하나지만, 회사 코드베이스는 웹 호환성 때문에 ES6는 아직 본격적으로 도입되지도 않았고, 대부분 jQuery 기반으로 간단한 프론트 로직만 넣을 뿐 비즈니스 로직은 백엔드에 있다보니 심도있게 쓸 일이 없었다.
하지만 얼마전 node.js로 간단한 스크립트를 만드는데 promise를 좀 쓰던중 계속 에러가 안 잡히고 프로그램이 강종되는 현상이 발생해서 제대로 공부를 해봤는데, promise에서는 throw e
형식으로 에러를 처리하면 골치아파지는걸 알게 되었다.
개요
일단, promise error handling 관련해서 구글링을 해보면 이런저런 글이 나온다. 해당 글들의 내용은 아래와 같다.
- Promise 에러핸들링 방법 1
const throwsError = () => new Promise((resolve, reject) => {
throw "Error!";
});
- Promise 에러핸들링 방법 2
const rejectsError = () => new Promise((resolve, reject) => {
reject("Error!");
});
위의 코드는 동일하다고 봐도 무방하며, 아래의 두가지 방식중 하나로 사용하면 된다.
// await를 사용하기 위해서는 async 필수
async function tryAwait() {
try {
await throwsError(); // 비동기함수는 await를 쓰지 않으면 catch블록에 걸리지 않는다.
} catch (e) {
console.log(e);
}
console.log("Error caught safely");
}
tryAwait();
혹은
throwsError().catch(e => {
console.log(e);
});
지금 말하고자 하는건 try...catch
안에 await로 에러를 잡을 것인지, .catch
체인으로 잡을 것인지에 대한건 아니지만 간략하게 차이점을 설명해보자면,
try...catch
로 promise/async 함수 에러를 잡을 때는 await
로 실행해야 하며, await
를 쓰기 위해서는 반드시 해당 코드가 async function
안에 있어야 한다. 따라서 위의 throwsError
나 rejectsError
함수를 await
로 실행하는 tryAwait()
함수 또한 async
로 선언되어야 하는 것이다. 하지만 이는 동기성이어야 하는 함수에서 사용하려면 해당 함수가 비동기로 바뀌어야 하고, 그러면 로직이 꼬이기에 해당 함수의 사용자까지 모두 바뀌고... 이는 결국 스크립트 전체가 비동기로 바뀌어야 하는 불상사를 초래한다.
하지만 사용처를 비동기로 바꾸지 않아도 에러를 잡는 방법이 있는데, Promise의 .catch
메소드를 사용하면 된다.
자, 그럼 본론으로 돌아가서.. Promise 에러핸들링 방법 1의 throw e
방식에는 어떤 문제가 있을까?
바로, 해당 비동기 함수 내의 또다른 비동기 로직 안에서 throw할 경우 프로그램 전체가 강종된다는 것이다. 예시를 보면 이해하기 쉽다.
async function someOtherAsyncFunction(success) {
if (success) {
return success;
} else {
throw "Error!";
}
}
const throwsErrorIncorrectly = () => new Promise((resolve, reject) => {
someOtherAsyncFunction(false).then(result =>
resolve(result)
).catch(e => {
throw e;
});
});
throwsErrorIncorrectly().catch(e => {
console.log(e);
}
위 코드를 실행할 경우, UnhandledPromiseRejectionWarning을 보게 된다. 이게 뜰 때는 상위에 어떤 에러핸들러가 있든간에 스크립트 전체가 죽게된다(아예 다른 콜스택이기에 상단에서 try...catch
나 .catch
등으로 잡히지 않음).
throwsErrorIncorrectly.catch
에서 에러가 잡히지 않은 이유는, someOtherAsyncFunction.catch()
에서 throw e
된 것은 someOtherAsyncFunction()
에서 생성된 별도 콜스택에서 발생한 에러이기 때문이다.
이는 아래와 같이 변경해야 정상동작하게 된다.
async function someOtherAsyncFunction(success) {
if (success) {
return success;
} else {
throw "Error!";
}
}
const rejectsErrorCorrectly = () => new Promise((resolve, reject) => {
someOtherAsyncFunction(false).then(result =>
resolve(result)
).catch(e =>
reject(e)
);
});
rejectsErrorCorrectly().catch(e => {
console.log(e);
});
... 따라서, Promise/비동기함수를 사용할 땐, 어차피 일반적인 문맥에서 동일한 효과를 지니는 throw e
와 reject(e)
중에서 reject(e)
를 쓰는게 정신건강에 이롭다.
FAQ: 어차피 일반적인 사람은 비동기 쓰면 reject()
쓸텐데 이걸 왜 알아야 돼?
- 코딩을 하다보면 자연스럽게 외부 라이브러리를 사용하게 되는데, 이 때 Promise를 지원하는 라이브러리만 쓰기는 쉽지 않다. Promise를 사용하지 않으면서 비동기성인 로직들은 백이면 백 callback을 사용하는데, 여기서 에러가 터지는 경우엔 위 내용을 알아야 대처할 수가 있다. 또 예시를 들어보자면 아래와 같다.
const doRequest = (settings) => new Promise((resolve, reject) => {
request({...settings}, (req, res, body) => {
const jsonBody = JSON.parse(body);
resolve({"headers": res.headers, "body": jsonBody});
}).on('response', response => {
response.headers['content-encoding'] = 'deflate';
}).on('error', err => reject(err))
});
위 코드에는 치명적인 문제가 있는데, 바로 JSON.parse(body)에서 SyntaxError 등이 발생할 경우 throw된 에러가 생겨서 프로그램이 죽어버린다는 것.
따라서 이를 아래와 같이 바꿔줘야 한다.
const doRequest = (settings) => new Promise((resolve, reject) => {
request({...settings}, (req, res, body) => {
try {
const jsonBody = JSON.parse(body);
} catch (e) {
reject(e);
}
resolve({"headers": res.headers, "body": jsonBody});
}).on('response', response => {
response.headers['content-encoding'] = 'deflate';
}).on('error', err => reject(err))
});
알고보면 무지 쉬운 내용이지만... 책 제대로 안 보고 대충 실무코드 보면서 체득한 경우엔 골치아플 수 있는 부분이다.
끗.