오늘은 unhandled promise error를 내면 안 되는 이슈를 알아보며 async/await과 error의 특징에 대해 살펴보겠습니다.
async function sub() {
throw 'hello';
}
async function main() {
await sub();
}
main();
브라우저: Uncaught (in promise) hello
노드: [UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "hello".] {
code: 'ERR_UNHANDLED_REJECTION'
}
노드의 에러 메시지가 더 길지만 둘 다 결국은 Promise에 catch를 달지 않았다는 에러(unhandled promise)입니다.
다음과 같이 try/catch문을 쓰면 콘솔에 caught hello라고 뜨며 에러를 잡을 수 있습니다.
async function sub() {
try {
throw 'hello';
} catch (err) {
console.log('caught', err);
}
}
async function main() {
await sub();
}
main();
try/catch문을 main에 써도 될까요?
async function sub() {
throw 'hello';
}
async function main() {
try {
await sub();
} catch (err) {
console.log('caught', err);
}
}
main();
네 이번에도 제대로 잡히는 것을 볼 수 있습니다.
Promise에서 throw된 것도 상위 함수로 전파되며 상위에서 try/catch해도 됩니다.
그러면 다음과 같이 최상위 스코프에서 잡을 수 있을까요?
async function sub() {
throw 'hello';
}
async function main() {
await sub();
}
try {
main();
} catch (err) {
console.log('caught', err);
}
다시 uncaught (in promise) hello 에러가 뜨게 됩니다. 이 에러는 사실 다음 코드에서도 발생합니다.
async function sub() {
throw 'hello';
}
async function main() {
try {
sub();
} catch (err) {
console.log('caught', err);
}
}
main();
아까는 에러가 잡혔던 것 같은데 무엇이 달라진 걸까요? 바로 sub() 앞에 await이 빠졌다는 것입니다. await이 빠진 경우 try/catch문으로는 잡을 수 없게 됩니다.
이렇게 main 앞에도 await을 붙이면 에러가 잡힙니다. async 함수 없이 main 앞에 await을 붙이는 건 ESM에서 가능한 top level await 문법입니다(노드 CJS에서는 안 됩니다)
async function sub() {
throw 'hello';
}
async function main() {
await sub();
}
try {
await main();
} catch (err) {
console.log('caught', err);
}
Unhandled promise 에러가 무서운 이유는 한 번 터지면 상위에서도 잡히지 않는다는 점 때문입니다.
async function sub() {
throw 'hello';
}
async function main() {
try {
sub();
} catch (err) {
console.log('caught', err);
}
}
try {
await main();
console.log('done');
} catch (err) {
console.log('caught', err);
}
실행해보면 done이 콘솔에 찍히고 그 다음에 unhandled promise 에러가 발생합니다.
main 실행 시 sub();에서 unhandled promise 에러가 발생했지만 try/catch문에서 잡히지 않는 것입니다.
따라서 await을 반드시 붙여주어야 하고, await을 붙이지 않은 경우는 적어도 .catch() 메서드를 붙여주어야 합니다.
async function sub() {
throw 'hello';
}
async function main() {
sub().catch((err) => console.log('caught', err));
}
main();
이 방법은 지저분하니 await을 꼭 붙여줍시다.
return이 있을 때도 마찬가지입니다.
async function sub() {
throw 'hello';
}
function main() {
try {
return sub();
} catch (err) {
console.log('caught', err);
}
}
main();
위와 같이 하면 에러가 잡히지 않으니 다음과 같이 async/await을 도입해야 합니다. Nest.js를 쓸 때 많이 실수하던 부분이었습니다.
async function sub() {
throw 'hello';
}
async function main() {
try {
return await sub();
} catch (err) {
console.log('caught', err);
}
}
main();
return 뒤에 await을 붙이면 됩니다.
콜백 함수가 async인 경우도 살펴봅시다.
async function sub(cb) {
await cb();
}
async function main() {
await sub(async function cb() {
throw 'callback';
});
}
try {
main();
console.log('done');
} catch (err) {
console.log('caught', err);
}
실행하면 역시나 done이 출력된 후에 unhandled promise 에러가 발생합니다.
이제 아시겠지만 unhandled promise 에러는 try/catch문에서 잡히지 않습니다.
async function sub(cb) {
await cb();
}
async function main() {
await sub(async function cb() {
throw 'callback';
});
}
try {
await main();
} catch (err) {
console.log('caught', err);
}
해결책도 아시겠죠? main 앞에 await을 붙이면 됩니다. 또는 중간 어디에서든 await을 쓰는 곳에서 try / catch로 잡으면 됩니다.
async function sub(cb) {
try {
await cb();
} catch (err) {
console.log('caught', err);
}
}
async function main() {
await sub(async function cb() {
throw 'callback';
});
}
await main();
async function sub(cb) {
await cb();
}
async function main() {
try {
await sub(async function cb() {
throw 'callback';
});
} catch (err) {
console.log('caught', err);
}
}
await main();
이번 글을 요약하면 다음과 같습니다.
- await의 에러는 상위 함수로 전파되며 try/catch문으로 잡을 수 있다
- await을 붙이지 않고 프로미스에서 에러가 발생하면 unhandled promise 에러가 발생한다.
- unhandled promise 에러는 try/catch문에서 잡히지 않으니 주의하자
- await을 붙이지 않은 프로미스는 try/catch문으로 에러를 잡을 수 없다. await이나 catch()를 꼭 붙여야 한다.