JavaScript에서 “’throw’ of exception caught locally” 경고가 뜬다면 — 예외를 제어 흐름에 쓰지 말 것

검증 환경

본 절은 원문 사례·문서에 등장한 버전/도구를 정리한 것이다. 별도 실험실 재현이 명시되지 않은 항목은 일반화하지 않는다.

  • 본문에 명시된 오류 메시지·프레임워크 버전을 기준으로 원인을 추적했다.
  • 문서 정리일: 2026-08-28
  • OS/CI 세부 값은 프로젝트마다 다르므로, 적용 전 로컬에서 동일 오류 메시지를 재확인한다.

1. 문제 정의

REST API 요청을 처리하는 async 함수에서, 내부의 여러 헬퍼 함수가 예외를 던져 “에러 응답을 보내야 한다"는 신호로 사용하고 있었다. 그런데 WebStorm(인텔리J IDEA 계열)이 throw 문에 다음과 같은 인스펙션 경고를 표시했다.

'throw' of exception caught locally — This inspection reports any instances of JavaScript throw statements whose exceptions are always caught by containing try statements. Using throw statements as a “goto” to change the local flow of control is likely to be confusing.

즉, 예외가 항상 자신을 감싸고 있는 try 문에 의해 포착되므로, 이 throw는 함수 밖으로 아무것도 전파하지 않는다. 그리고 예외를 “goto"처럼 로컬 제어 흐름을 바꾸는 용도로 쓰는 것은 오해를 불러일으키기 쉽다는 뜻이다.

문제가 되는 코드는 아래와 같다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
static async handleRequest(req) {
    try {
        let isAllowed = await checkIfIsAllowed(req);
        if (!isAllowed) {
            throw new ForbiddenException("You're not allowed to do that.");
        }
        let result = await doSomething(req); // can also raise exceptions
        sendResult(result);
    } catch(err) {
        sendErrorCode(err);
    }
}

2. 표면 증상

  • 에러가 아니라 정적 분석 경고다. 실행 시 오류가 아니라 WebStorm이 편집기에서 throw에 밑줄을 긋는 것.
  • isAllowed === false인 지점에서 던진 ForbiddenException은 오직 아래 catch(err)에서만 잡힌다.
  • 외부 호출자(전달되는 API 레이어)는 이 예외가 발생하는 것을 알 방법이 없고, 발생해도 볼 수 없다.
  • 질문 작성자는 “예외를 잘못 쓰고 있는지, 아니면 경고가 잘못 나오는 것인지” 헷갈렸다.

2.1 증상 fingerprint

항목내용
진단 도구WebStorm / JetBrains IDE 인스펙션
메시지'throw' of exception caught locally
발생 단계편집/정적 분석 단계 (실행 아님)
관련 기술JavaScript, async/await, ES6+ class static method
핵심 트리거throw가 같은 함수 안의 try에 의해 반드시 잡히는 구조
흔한 오해“경고니까 끄면 된다” (실제로는 설계 문제 신호)

※ 이 경고의 정확한 문구와 의미는 WebStorm 공식 인스펙션 설명 및 해당 인스펙션 문서 기준으로 확인할 수 있다. 인텔리J IDEA의 “throw of exception caught locally” 인스펙션은 동일한 시맨틱스를 공유한다.

2.2 당장 확인할 것

  1. 내가 던진 예외가 같은 함수(또는 곧바로 감싼 블록)의 catch에서만 잡히는가?
  2. 그 예외가 이 함수의 바깥 호출자까지 전파되어야 하는지?
    • 아니라면 → 아래 “원인 추적 / 근본 원인” 참고
    • 맞아서 같은 try에서 잡는 것이라면 → 리팩토링 대상

3. 원인 추적

질문 본문의 코드를 추적해 보면 두 종류의 예외 경로가 섞여 있다.

  1. 진짜로 예외를 전파해야 하는 경로: doSomething(req)나 checkIfIsAllowed(req) 같은 호출된 함수가 내부적으로 예외를 던진다는 것은, 이 함수가 자체적으로는 처리하지 못할 문제(네트워크 오류, DB 장애, 예상치 못한 실패)가 발생했다는 의미다. 이런 파훼된 예외는 catch(err) -> sendErrorCode(err)로 잡혀 에러 응답으로 변환된다. 이것이 정상적인 예외 사용이다.

  2. 예외를 이용한 인위적인 점프: if (!isAllowed) throw new ForbiddenException(...). 여기서는 예외를 던질 필요가 전혀 없다. 이미 이 상황에서 할 일(sendErrorCode 호출)을 알고 있기 때문이다. 그런데도 굳이 throw -> catch 경로를 돌려 동일한 동작을 수행하고, 그러는 바람에 예외가 “항상 로컬의 catch에 잡히는” 구조가 만들어졌다.

원인 후보 매트릭스

원인 후보확인 방법맞는 경우의 증상해결 방향
예외를 로컬 분기(goto)로 사용throw가 항상 같은 함수 상단 try에 노출되는지 확인WebStorm 경고 발생직접 sendErrorCode 후 return (아래 해결책)
진정한 예외를 의도한 것이라 경고가 오독throw되는 예외가 외부에 전파 constraint가 필요한지 확인예외가 실제로 바깥으로 전파됨(해당 시) 경고를 없애려면 예외를 로컬에서 직접 처리
코드 중복을 피하려는 의도(DRY)catch 블록 코드를 분기에 복사하고 싶은지질문 본문에서 “복붙하면 유지보수 어려움” 언급분기에 직접 sendErrorCode + return, 함수 추출 등으로 중복 관리

4. 근본 원인

근본 원인은 “예외를 관리하는 방식"이 아니라, 예외를 제어 흐름(일부 로컬 “goto”)으로 사용한 설계다.

  • 예외는 정말 예외적이고 처리 방법을 모르는 상황에서 바깥 호출자에게 상황을 전달하기 위한 메커니즘이다.
  • 이 코드처럼 “이미 어떤 처리를 해야 할지 안다” 그리고 **“그 처리를 try 아래 catch에서 할 것”**이라면, throw를 통해 예외로 갔다가 다시 돌아오는 것은 불필요한 간접화다.
  • 때문에 IDE는 “throw가 항상 같은 try에 잡히는 구문은 goto와 다름없다"고 판단해 경고를 낸다.

즉, 예외를 이미 내가 처리할 것으로 아는 상황에서 throw/catch로 제어를 우회시키는 패턴 자체가 문제의 핵심이다.

5. 코드 해결책

원칙: 예외를 이미 알고 내가 직접 대응할 수 있는(로컬 처리 로직이 명확한) 분기에는 throw가 아니라 직접 처리 후 return 한다. throw는 “어떻게 해결해야 할지 모르는 진정한 예외 상황"에서만 사용한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
static async handleRequest(req) {
    try {
        let isAllowed = await checkIfIsAllowed(req);
        if (!isAllowed) {
            // 이미 처리 방법을 알고 있으므로 throw 없이 직접 처리 후 종료
            sendErrorCode("You're not allowed to do that.");
            return;
        }
        let result = await doSomething(req); // 내부에서 객관적으로 예외를 던질 수 있다.
        sendResult(result);
    } catch(err) {
        sendErrorCode(err);
    }
}

5.1 코드 변화 정리

항목이전이후
허용 거부 분기throw new ForbiddenException("...")sendErrorCode("You're not allowed to do that."); return;
제어 흐름throw -> catch 위를 우회분기에서 즉시 응답 후 반환
catch 베이스예외를 처리(여전히 진짜 예외 처리용)
WebStorm 경고throw에 밑줄사라짐

5.2 “코드 중복(DRY)이 걸리는데 어떻게 하지?”

sendErrorCode 로직을 두 곳(분기 + catch)에 쓰는 것이 걸리수도 있다. 하지만 이 중복은 단순히 같은 함수/함수를 “반환 후 마무리"하는 공통 종료 경로일 뿐이며, 읽기와 유지보수에 더 유리하다. 중복을 우려할 만큼 크다면 공통 에러 응답 헬퍼를 추출하는 것으로 관리하면 된다. (실용적인 리팩토링 기준이며, 환경에 따라 중복 관리 방식은 팀 컨벤션을 따라간다.)

6. 재발 방지

throw를 마구 사용하지 않기 위해 다음과 같은 기준을 점검한다.

  1. 이 함수 내부에서 즉시 대응할 수 있는 상황인가? → 그러면 throw 대신 직접 처리 후 return.
  2. 이 예외는 외부 호출자가 처리할 필요가 되는가? → 그러면 throw로 전파하고, 발생 지점에서는 (감싼 try 없이) 그대로 던지는 것이 맞다.
  3. throw가 반드시 같은 함수 상단의 catch에만 걸리면 경고 표시의 근거다 — 리팩토링 대상.
  4. IDE가 유사한 경고를 잡도록 인스펙션(예: Throwable stuck inside try)을 켜두면 재등장을 조기에 발견.

실전 적용 체크

  • 로컬: 리팩토링 후 isAllowed=false 경로에 sendResult가 실행되지 않는지 확인(에러 응답 후 return).
  • CI: 정적 분석(WebStorm/ESLint 계열)에서 해당 인스펙션 경고가 사라졌는지(= 0 warning) 것을 게이트로 사용.
  • 프로덕션: doSomething이 진짜 예외를 던졌을 때만 catch가 동작하고, 그 외엔 sendErrorCode가 한 번만 호출되는지 로그/테스트로 검증.

잘못된 해결책

  • 경고 인스펙션을 그대로 비활성화하기: 표면 증상만 숨길 뿐, 예외를 goto로 쓰는 설계가 남는다. 이후 추적이 어려워지고 유지보수 취약성이 남는다.
  • 무조건 try/catch를 완전히 제거하기: doSomething 등에서 진짜 예외가 나는 경로까지 잡지 못하게 되면 손님 요청에 예외 에러가 전파된다.

검증 명령

리팩토링 후 예외 처리 흐름을 실행 테스트로 검증한다(브라우저/Node 환경에 따라 실행).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 허용 거부(권한 없음) 경로가 예외를 던지지 않고 에러 응답으로 끝나는지 확인
node -e "
const assert = require('assert');
async function checkIfIsAllowed(){ return false; }
function sendResult(r){ console.log('RESULT', r); }
function sendErrorCode(e){ console.log('ERROR', e); }
class ForbiddenException extends Error {}
class H {
  static async handleRequest(req){
    try {
      let isAllowed = await checkIfIsAllowed(req);
      if(!isAllowed){ sendErrorCode(\"You're not allowed to do that.\"); return; }
      let result = 'ok'; sendResult(result);
    } catch(err){ sendErrorCode(err); }
  }
}
H.handleRequest({});
// 기대: 'ERROR ...' 한 번, 'RESULT'는 나오지 않아야 함
"
  • 기대 결과: 권한 거부 시 sendErrorCode가 한 번만 호출되고 sendResult는 호출되지 않는다.
  • 그리고 handleRequest가 바깥으로 예외를 던지지 않음(예외가 “로컬에 잡힌 채” 사라지지 않도록 명시적으로 처리).

DevTrace verdict

이 경고의 핵심은 실행 오류가 아니라 설계 신호이며 — 이미 처리 방법을 알고 있는 로컬 분기에서는 throw 대신 직접 처리 후 return을 쓰고, throw는 진짜 예외 상황에서만 사용하는 것이 정석이라는 것이다.


출처:

DevTrace 결론

같은 try 안에서 throw 되고 항상 catch 되는 예외는 throw가 아니라 직접 처리 후 return으로 대체해야 하며, 반면 외부 호출자에게 전파돼야 하는 진정한 예외 상황에만 throw를 사용해야 합니다.

원문 출처는 문제 발견의 단서이며, 위 판단과 점검 항목은 DevTrace의 독자 분석이다.