async-await와 try-catch

Yeonjoo Yoo·2022년 1월 24일
8

TIL

목록 보기
1/12
post-custom-banner
우연히 한 블로그에서 "각 태스크의 실행 순서가 중요한 경우라면 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

profile
to be frontend engineer
post-custom-banner

0개의 댓글