async 함수와 try-catch

joonseokhu·2020년 5월 24일
30

TL;TR

async 함수를 쓸 때 습관적으로 무조건 try-catch 문을 쓰는 사람들이 많이 있다.
하지만 async 에서 try-catch 로 전체 코드를 묶고 catch 문에서 throw e 하는 것과 try-catch문을 아예 쓰지 않는것은 흐름상으로나 결과적으로나 완전히 같은 결과를 만들어낸다.

많은 사람들이 습관적으로 쓰는 async 함수의 스타일

async 함수를 쓸 때 습관적으로 무조건 try-catch 문을 쓰는 사람들이 많이 있다.

const func = async () => {
  try {
    const value = await someFunc();
    const result = await anotherFunc();
    return result;
  } catch (err) {
    throw err;
  }
}

이런 식으로 async 함수에서 발생하는 비동기 에러를 다시 전달하기 위해 try-catch 로 묶고, catch 에서 에러를 다시 던지는 코드를 많이 발견할 수 있다.

하지만 만약 try-catch 문 없이 그냥 async 함수를 쓴다면 무슨 차이가 생길까?

실험해보기

const makeError = async () => { throw new Error('에러 클래스에 의한 에러') }

const withTryCatch = async () => {
  try {
    console.log('try-cath 를 사용한 async')
    const result = await makeError();
    console.log('withTryCatch - 에러가 발생하는 위치 아래에 있는 코드 (실행되면 안됨)');
    return result;
  } catch (err) {
    throw err;
  }
}

const withoutTryCatch = async () => {
  console.log('try-cath 없는 async')
  const result = await makeError();
  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)
})

withTryCatchwithoutTryCatch 는 둘다 async 함수이다.
withTryCatch는 사람들이 많이 쓰는것처럼 try-catch 문으로 함수 안쪽 코드를 묶고 catch 문에서 받은 에러를 던졌고, withoutTryCatch는 그냥 작성했다.

실험 결과

'try-cath 를 사용한 async'
Promise { <pending> }
'try-cath 없는 async'
Promise { <pending> }
'withTryCatch - 실패결과' '에러 클래스에 의한 에러'
'withoutTryCatch - 실패결과' '에러 클래스에 의한 에러'

try-catch 를 쓰든 안쓰든 상관없이

  • 에러가 발생하는 코드 다음에 있던 코드들은 실행되지 않는다.
  • 발생한 에러는 Promise.reject 처리되어 상위 컨텍스트에서 비동기 에러로 처리된다.

상위 컨텍스트로 에러를 전파하기 위해 async 함수의 내부를 try-catch로 묶을 필요는 없다.
즉, 상위 컨텍스트로 에러를 전파하기 위한 try-catch 문은 필요없는 코드인 것이다.

Node.js / Express.js 에서의 적용

NodeJS 에서 비동기 함수를 작성할 때 대부분의 경우 try catch 문을 작성하지 않는게 오히려 바람직하다.

다만 유일하게 try-catch를 꾸준히 써야 하는 부분이 있는데 바로 컨트롤러 레이어 로직이다.

비동기 코드가 성공하든 에러가 발생하든 결국 클라이언트에게는 정상적인 응답을 내주어야 한다. 그 응답이 해당 요청을 수행할 수 없다는 에러이더라도 말이다.

때문에, 서비스로직까지는 비동기에러를 계속 try-catch 없이 그대로 전파해 주는게 바람직하고,
컨트롤러에선 try-catch를 통해 더 이상 에러가 전파되는것을 차단하고 에러내용을 정리해 400~500번대 상태코드와 함께 응답을 해주는 것이다.

ExpressJS 의 경우 이 컨트롤러 레이어는 미들웨어 함수에 해당한다.

app.get('/foo', async (req, res, next) => {
  try {
    // 컨트롤러 로직
    
    // 비동기 결과가 reject 라면 catch 문으로 점프
    const some = await something();
    
    // 비동기 결과가 reject 라면 catch 문으로 점프.
    const another = await anotherThing();
    
    if (another.result > 30) {
      // Promise.reject를 리턴해도 async 함수에선 에러가 던져진것과 동등한것으로 간주된다.
      return Promise.reject({
        message: '뭔가 비즈니스적으로 30을 초과하면 안되는 그런 것',
        status: 403,
      })
    }
    
    // 앞쪽에서 아무런 문제도 없어야 성공결과가 응답된다.
    res.json(another);
    
  } catch (err) {
    // 여기에선 따로 throw err를 하지 않는다.
    // 상위 컨텍스트로 에러를 전파하는게 아니라 클라이언트로 에러를 응답해야 하는 레이어이다.
    res.status(err.status || 500).json({
      message: err.message || 'unknown error'
    })
  }
});
profile
풀스택 집요정

1개의 댓글

comment-user-thumbnail
2024년 1월 9일

좋은 글 잘보고 갑니다.

답글 달기