async-await와 try-catch
우연히 한 블로그에서 "각 태스크의 실행 순서가 중요한 경우라면 async-await를 사용하자!"
라는 문장을 보게 되었고, 며칠 전 사이드 프로젝트에서 SNS Login 했던 코드가 생각나게 되었다.
코드의 양을 줄일 수 있고 명확하게 작성할 수 있을 거라 판단되어 async-await를 적용하기로 했다.
함수 앞에 async를 붙이고 try-catch를 추가하면서 궁금하기 시작했다.
"async-await에 try-catch는 필수인가?"
try-catch
- try-catch 문을 사용하면 에러 발생 시 스크립트가 죽는 것을 방지하고, 에러 상황을 잡아 예외처리를 할 수 있음
- 자바스크립트에서 에러가 발생되면 코드는 멈추게 되고, 콘솔에 에러가 출력됨
- JS single thread로 동작 언어로, 동기 처리에서는 정상적으로 try-catch로 오류를 잡아 에러 처리할 수 있음. 하지만, 비동기 처리에는 문제가 발생할 수 있음
try {
setTimeout(() => { throw new Error('error!'); }, 1000);
// 해결 방법
// setTimeout(() => {
// try {
// throw new Error('error!');
// } catch(err) {
// console.log(err);
// }
// }, 1000);
} catch(err) {
console.log(err);
}
기본 예시
try-catch가 없는 경우
- reject 발생 이후 코드는 실행되지 않고, 콘솔에 Error가 출력된 후 실행 종료
실행 코드
const asyncFn = async () => {
console.log('start');
const rj1 = await rejectFn1();
console.log(rj1);
const rj2 = await rejectFn2();
console.log(rj2);
}
const rejectFn1 = () => Promise.reject('rejectFn1 rejected');
const rejectFn2 = () => Promise.reject('rejectFn2 rejected');
asyncFn()
.then(() => {
console.log('next asynFn()');
})
실행 결과
- Console
start
Uncaught (in promise) rejectFn1 rejected - 평가
Promise {<rejected>: 'rejectFn1 rejected'}
상위 컨텍스트에 catch()를 사용한 에러 처리
- asyncFn 함수는 Promise를 반환하므로 promise chain을 사용하여 에러 처리 가능
- 상위로 오류를 전파
실행 코드
const asyncFn = async () => {
console.log('start');
const rj1 = await rejectFn1();
console.log(rj1);
const rj2 = await rejectFn2();
console.log(rj2);
}
const rejectFn1 = () => Promise.reject('rejectFn1 rejected');
const rejectFn2 = () => Promise.reject('rejectFn2 rejected');
asyncFn()
.then(() => {
console.log('next asynFn()');
})
.catch((e) => {
console.log('asyncFn catch', e);
})
실행 결과
- Console
start
asyncFn catch rejectFn1 rejected - 평가
Promise {<fulfilled>: undefined}
try-catch를 사용한 에러 처리
- asyncFn 함수 내부를 try-catch로 감싼 경우
- try 블록 안에서 reject 발생하면 즉시 코드의 실행이 중단되고 catch 블록으로 제어 흐름이 넘어감
- catch에서 에러를 처리하기 때문에 스크립트는 죽지 않고 Promise <fulfilled>을 반환하여 then() 이 실행됨
실행 코드
const asyncFn = async () => {
try {
console.log('start');
const rj1 = await rejectFn1();
console.log(rj1);
const rj2 = await rejectFn2();
console.log(rj2);
} catch (err) {
console.log('catch in asyncFn -', err)
}
}
const rejectFn1 = () => Promise.reject('rejectFn1 rejected');
const rejectFn2 = () => Promise.reject('rejectFn2 rejected');
asyncFn()
.then(() => {
console.log('next asynFn()');
})
.catch((e) => {
console.log('asyncFn catch', e);
})
실행 결과
- Console
start
catch in asyncFn rejectFn1 rejected
next asynFn() - 평가
Promise {<fulfilled>: undefined}
비교 예시
try-catch가 있는 경우와 아닌 경우 비교
- try-catch가 없는 코드에서 발생한 에러는 Promise.reject 처리되어 상위 컨텍스트에서 비동기 에러로 처리
- 상위 컨텍스트로 에러를 전파하여 error handle을 하는 경우는 try-catch 문이 필요 없음
- 만약 try-catch를 사용하면, catch 블럭에서 상위로 throw error 해야함
- try-catch에 상관없이 에러가 발생 이후 코드들은 실행되지 않음
- 두 함수 모두 Promise {<fulfilled>: undefined}로 평가됨
실행 코드
const getError = async () => { throw new Error('error!'); }
const withTryCatch = async () => {
try {
console.log('try-cath 사용한 async');
const result = await getError();
console.log('withTryCatch - 에러 다음 코드 (실행되면 안 됨)');
return result;
} catch (err) {
throw err;
}
}
const withoutTryCatch = async () => {
console.log('try-cath 없는 async');
const result = await getError();
console.log('withoutTryCatch - 에러 다음 코드 (실행되면 안 됨)');
return result;
}
withTryCatch()
.then(res => {
console.log('withTryCatch - 성공', res);
}).catch(err => {
console.log('withTryCatch - 실패', err.message);
});
withoutTryCatch()
.then(res => {
console.log('withoutTryCatch - 성공', res);
})
.catch(err => {
console.log('withoutTryCatch - 실패', err.message);
})
실행 결과
- Console
try-cath 사용한 async
try-cath 없는 async
withTryCatch - 실패 error!
withoutTryCatch - 실패 error! - 평가
Promise {<fulfilled>: undefined}
각 비동기 코드에 대한 에러 처리
try-catch
- 각 await를 try-catch로 감싸 error 처리 가능
- 코드 가독성 떨어짐
실행 코드
const asyncFn = async () => {
console.log('start');
try {
const rj1 = await rejectFn1();
console.log(rj1);
} catch (err) {
console.log(`catch in rejectFn1 - ${err}`);
}
try {
const rj2 = await rejectFn2();
console.log(rj2);
} catch (err) {
console.log(`catch in rejectFn2 - ${err}`);
}
}
const rejectFn1 = () => Promise.reject('rejectFn1 rejected');
const rejectFn2 = () => Promise.reject('rejectFn2 rejected');
asyncFn()
.then(() => {
console.log('next asynFn()');
})
.catch((e) => {
console.log('asyncFn catch', e);
})
실행 결과
- Console
start
catch in rejectFn1 - rejectFn1 rejected
catch in rejectFn2 - rejectFn2 rejected
next asynFn() - 평가
Promise {<fulfilled>: undefined}
catch()
- async 함수 promise를 반환하므로 Promise.prototype에 메서드를 모두 사용 가능
- Promise.prototype.catch에 접근하여 사용할 수 있음
- 각 비동기 함수 별로 명확하게 에러 처리 할 수 있고, 상수를 유지할 수 있음
실행 코드
const asyncFn = async () => {
console.log('start');
const rj1 = await rejectFn1().catch(err => { return `catch in rejectFn1 - ${err}`; });
console.log(rj1);
const rj2 = await rejectFn2().catch(err => { return `catch in rejectFn2 - ${err}`; });
console.log(rj2);
}
const rejectFn1 = () => Promise.reject('rejectFn1 rejected');
const rejectFn2 = () => Promise.reject('rejectFn2 rejected');
asyncFn()
.then(() => {
console.log('next asynFn()');
})
.catch((e) => {
console.log('asyncFn catch', e);
})
실행 결과
- Console
start
catch in rejectFn1 - rejectFn1 rejected
catch in rejectFn2 - rejectFn2 rejected
next asynFn() - 평가
Promise {<fulfilled>: undefined}
결론
- try-catch는 에러 처리 시 중요하지만, 비동기에서는 조심히 사용해야 함
- 상위 컨텍스트로 에러를 전파하여 처리하는 경우 try-catch 문은 필요 없는 코드
- 각 비동기 함수에 에러 처리가 필요한 경우 Promise.prototype.catch 활용
참고
node.js
- 컨트롤러 레이어 로직에서 try-catch 문을 사용해야 함
- 비동기 코드의 성공 여부와 관계 없이 결국 클라이언트에게는 정상적인 응답을 내주어야 하기 때문
- 서비스 로직까지는 비동기 에러를 try-catch 없이 계속 그대로 전파해 주는게 바람직하고, 컨트롤러에선 try-catch를 통해 더 이상 에러가 전파되는 것을 차단하고 에러 내용을 정리해 400~500번대 상태 코드와 함께 응답을 해주는 것
const anotherThing = async (some) => {
if (some.cnt === 0) {
return Promise.reject({
message: '서비스 로직 에러',
status: 403,
})
}
return Promise.resolve({ message: '성공' })
}
// 컨트롤러 로직
app.get('/foo', async (req, res, next) => {
try {
// 비동기 결과가 reject 라면 catch 문으로 점프
const some = await something();
// 비동기 결과가 reject 라면 catch 문으로 점프
const another = await anotherThing(some);
// 앞쪽에서 아무런 문제도 없어야 성공 결과가 응답됨
res.json(another);
} catch (err) {
// throw err를 하지 않음 (상위 컨텍스트로 에러를 전파하지 않음)
// 클라이언트로 에러를 응답
res.status(err.status || 500).json({
message: err.message || 'unknown error'
})
}
});
참고
https://velog.io/@vraimentres/async-%ED%95%A8%EC%88%98%EC%99%80-try-catch
https://merrily-code.tistory.com/214
https://itnext.io/async-await-without-try-catch-in-javascript-6dcdf705f8b1
https://itnext.io/error-handling-with-async-await-in-js-26c3f20bc06a
https://stackoverflow.com/questions/40884153/try-catch-blocks-with-async-await
https://blog.grossman.io/how-to-write-async-await-without-try-catch-blocks-in-javascript/
https://softwareengineering.stackexchange.com/questions/144326/try-catch-in-javascript-isnt-it-a-good-practice
https://programmingsummaries.tistory.com/375
Author And Source
이 문제에 관하여(async-await와 try-catch), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@yjyoo/async-await와-try-catch저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)