PHpullh

JAVASCRIPT · 심층 가이드

JavaScript 에러 처리 완전 정리

try/catch 한 겹으로 버티는 대신 사용자 정의 오류, 입력 가드, Result 반환, 다단계 검증으로 실패를 설계하는 JavaScript 코드 15편입니다.

주제 15개 · 예제 코드 포함 · 최종 수정 2026-09-03 · 작성 pullh 편집팀

JavaScript는 무엇이든 던질 수 있습니다. 문자열도, 숫자도, undefined도 throw의 대상이 됩니다. 그래서 catch (e) 안에서 e.message를 읽는 순간 또 다른 예외가 나는 코드가 심심찮게 나옵니다. 함수 시그니처에 어떤 오류가 나오는지 적을 자리도 없으니, 호출자는 문서나 코드를 직접 읽기 전에는 알 방법이 없습니다. 실패의 형태를 스스로 규정해야 하는 언어라는 뜻입니다.

try catch 기본에서 출발하되 사용자 정의 오류로 곧장 넘어가는 편이 좋습니다. Error를 상속한 클래스를 만들어 두면 도메인 오류 표현과 오류 다시 던지기가 자연스럽게 이어지고, 어떤 실패를 삼키고 어떤 실패를 올려보낼지 기준이 생깁니다. 검증 쪽은 입력 가드로 좁게 막고 다단계 검증으로 여러 규칙을 모으며, Result 반환은 예외 대신 값으로 실패를 표현하는 대안입니다.

놓치기 쉬운 부분이 finally 정리의 우선순위입니다. finally 블록 안에서 return이나 throw를 하면 try에서 결정된 반환값과 예외를 덮어써 버립니다. 리소스 해제 도중 실패한 오류가 원래 오류를 통째로 지우는 셈입니다. 원인을 남기려면 new Error(msg, { cause: err })로 연결하는 편이 낫습니다. parseInt("12abc")가 조용히 12를 돌려준다는 점도 안전한 숫자 파싱이 별도 항목인 이유입니다.

01try catch 기본

JSON.parse('{')가 던진 SyntaxError를 catch가 받아, Error 인스턴스인지 확인한 뒤 message를 출력합니다. 외부에서 들어온 문자열을 파싱할 때 이 감싸기가 필요합니다. catch에는 Error가 아닌 값도 던져질 수 있어서, 검사 없이 error.message를 읽으면 undefined가 나올 수 있습니다.

JavaScript code

// try catch 기본
try {
  JSON.parse("{");
} catch (error) {
  console.log(error instanceof Error ? error.message : String(error));
}
알아두면 좋은 점

catch의 바인딩은 생략할 수 있어, 오류 값을 쓰지 않을 때는 catch { ... }처럼 괄호 없이 적을 수 있습니다.

자주 하는 실수

try 블록을 넓게 잡으면 파싱 오류뿐 아니라 그 뒤 처리 코드의 버그까지 함께 삼켜집니다. 던질 가능성이 있는 줄만 감싸야 원인이 드러납니다.

02사용자 정의 오류

requireName은 trim()으로 공백을 없앤 결과가 비면 throw new Error('name required')를 하고, 아니면 원본 문자열을 그대로 반환합니다. 여기서는 'Ada'가 통과합니다. 사용자 정의 오류는 class X extends Error로 만들어야 하며, 그래야 호출한 쪽이 instanceof로 종류를 가려낼 수 있습니다.

JavaScript code

// 사용자 정의 오류
function requireName(name) {
  if (!name.trim()) throw new Error("name required");
  return name;
}
console.log(requireName("Ada"));
알아두면 좋은 점

class ValidationError extends Error를 만들 때 this.name을 함께 지정해야 로그와 스택 첫 줄에 오류 종류가 남습니다.

자주 하는 실수

throw "name required"처럼 문자열을 던지면 스택 추적이 만들어지지 않아 어느 줄에서 났는지 알 수 없습니다. 항상 Error 객체를 던져야 합니다.

03입력 검증

Number(value)로 변환한 뒤 Number.isNaN으로 실패를 판별해, 실패면 null을 성공이면 숫자를 돌려줍니다. 던지지 않고 값으로 실패를 알리는 검증 방식입니다. 전역 isNaN은 인자를 먼저 숫자로 바꿔서 isNaN('')이 false가 되는 반면, Number.isNaN은 진짜 NaN에만 true를 냅니다.

JavaScript code

// 입력 검증
const parseNumber = (value) => {
  const parsed = Number(value);
  return Number.isNaN(parsed) ? null : parsed;
};
console.log(parseNumber("3"));
알아두면 좋은 점

Number.isFinite를 쓰면 NaN뿐 아니라 Infinity까지 한 번에 걸러져, 나눗셈 결과를 검증할 때 더 촘촘한 가드가 됩니다.

자주 하는 실수

Number(null)과 Number([])는 모두 0이라 이 검증을 통과합니다. 문자열만 받을 자리라면 typeof 검사를 먼저 두어야 합니다.

04안전한 숫자 파싱

숫자 파싱이 아니라, 조건이 거짓일 때만 Error를 던지는 assert 함수를 정의하고 true로 한 번 호출해 통과시킵니다. 있어서는 안 될 상태를 코드로 못 박을 때 이 형태를 씁니다. 조건 자리에 함수 호출을 넣으면 검사 통과 여부와 무관하게 항상 실행되므로, 부수 효과가 있는 식은 넣지 않습니다.

JavaScript code

// 안전한 숫자 파싱
function assert(condition, message) {
  if (!condition) throw new Error(message);
}
assert(true, "must be true");
알아두면 좋은 점

메시지를 문자열 연결로 미리 만들면 검사에 통과할 때도 조립 비용이 듭니다. 비싼 메시지는 조건이 어긋났을 때만 만들도록 함수로 넘기는 방법이 있습니다.

자주 하는 실수

단언을 빌드에서 걷어 내는 설정을 쓰면서 조건 자리에 부수 효과가 있는 호출을 넣으면, 배포본에서 그 동작 자체가 통째로 사라집니다.

05안전한 나눗셈

나눗셈은 없고, 잘린 JSON 문자열을 파싱하다 난 오류를 잡아 메시지만 남기는 코드입니다. 자바스크립트의 나눗셈은 0으로 나눠도 예외 대신 Infinity를, 0 / 0이면 NaN을 돌려주기 때문에 이런 try/catch로는 걸리지 않고 결과값을 직접 검사해야 합니다.

JavaScript code

// 안전한 나눗셈
try {
  JSON.parse("{");
} catch (error) {
  console.log(error instanceof Error ? error.message : String(error));
}
알아두면 좋은 점

1 / 0은 Infinity, 0 / 0은 NaN입니다. 예외가 나지 않으므로 결과를 다시 계산에 넣기 전에 Number.isFinite로 걸러야 합니다.

자주 하는 실수

NaN은 자기 자신과도 같지 않아 result === NaN 검사는 언제나 거짓입니다. 이 조건문은 통과해 잘못된 값이 그대로 흘러갑니다.

06finally 정리

finally 블록은 없고, 이름이 비었는지 검사해 문제가 있으면 던지고 아니면 값을 돌려주는 함수만 있습니다. 정리 코드가 필요하면 try에 finally를 붙여 성공과 실패 양쪽에서 실행되게 합니다. finally 안에서 return하면 앞서 던진 오류가 삼켜지므로 그 블록에서는 반환하지 않아야 합니다.

JavaScript code

// finally 정리
function requireName(name) {
  if (!name.trim()) throw new Error("name required");
  return name;
}
console.log(requireName("Ada"));
알아두면 좋은 점

finally는 try 안에서 반환한 뒤에도 실행되므로, 반환값을 계산한 다음 자원을 닫는 순서가 자연스럽게 보장됩니다.

자주 하는 실수

finally 안에서 반환하거나 다시 던지면 앞서 발생한 오류가 덮여 사라집니다. 정리 코드에서 나는 오류도 따로 감싸야 원인이 남습니다.

07오류 다시 던지기

이 코드는 오류를 다시 던지지 않고, 변환 실패를 null이라는 반환값으로 바꿔 호출한 쪽에 넘깁니다. 실패가 흔하고 예상 가능한 경우에 어울리는 처리입니다. 반대로 다시 던질 때는 새 Error로 감싸되 cause 옵션에 원래 오류를 담아야 원인과 스택이 함께 남습니다.

JavaScript code

// 오류 다시 던지기
const parseNumber = (value) => {
  const parsed = Number(value);
  return Number.isNaN(parsed) ? null : parsed;
};
console.log(parseNumber("7"));
알아두면 좋은 점

new Error("설정 로드 실패", { cause: error })로 감싸면 상위 문맥과 원래 오류를 함께 남길 수 있고 error.cause로 되짚을 수 있습니다.

자주 하는 실수

catch (e) { throw e; }는 아무것도 더하지 않으면서 코드만 늘립니다. 문맥을 덧붙이지 않을 거라면 try를 아예 두지 않는 편이 낫습니다.

08도메인 오류 표현

도메인별 오류 타입 대신 메시지 문자열을 받는 일반 assert 하나로 조건을 확인합니다. 호출한 쪽이 오류 종류를 구분할 수 없다는 것이 이 방식의 한계이며, 이름을 붙인 하위 클래스를 만들면 catch에서 상황별로 다르게 대응할 수 있습니다. 메시지 문구로 분기하는 코드는 문구가 바뀌는 순간 깨집니다.

JavaScript code

// 도메인 오류 표현
function assert(condition, message) {
  if (!condition) throw new Error(message);
}
assert(true, "must be true");
알아두면 좋은 점

오류 종류를 code 필드로 구분하면 클래스를 늘리지 않고도 error.code === "NOT_FOUND"처럼 분기할 수 있고 문구 변경에 영향을 받지 않습니다.

자주 하는 실수

error.message.includes("not found")로 분기하면 문구를 다듬거나 번역하는 순간 조건이 조용히 거짓이 되어, 처리해야 할 오류가 그대로 흘러갑니다.

09fallback 값 사용

대체값을 돌려주지는 않고 catch가 오류 메시지를 출력하고 끝냅니다. 폴백을 쓰려면 catch 안에서 기본 객체를 반환하거나 미리 선언한 변수에 대입하면 됩니다. try 블록 안에서 let으로 선언한 변수는 블록 밖에서 보이지 않으므로 선언은 try 앞에 두어야 합니다.

JavaScript code

// fallback 값 사용
try {
  JSON.parse("{");
} catch (error) {
  console.log(error instanceof Error ? error.message : String(error));
}
알아두면 좋은 점

폴백을 쓸 때는 대체값을 썼다는 사실을 로그나 반환값에 남겨야, 원본이 계속 실패하고 있다는 것을 나중에라도 알아챌 수 있습니다.

자주 하는 실수

try 안에서 const로 선언한 변수는 블록 밖에서 보이지 않아 참조 오류가 납니다. 선언은 try 앞에 let으로 두어야 합니다.

10assertion

assert라는 이름은 없지만 if (!name.trim()) throw 한 줄이 사실상 같은 역할을 합니다. 함수 첫머리에서 전제를 확인하고 어긋나면 즉시 중단하는 방식입니다. name이 null이나 undefined면 trim() 호출 자체가 TypeError를 내므로, 그 앞에서 타입부터 걸러야 합니다.

JavaScript code

// assertion
function requireName(name) {
  if (!name.trim()) throw new Error("name required");
  return name;
}
console.log(requireName("Ada"));
알아두면 좋은 점

함수 첫머리에 전제를 모아 두면 본문의 중첩이 줄어듭니다. 조건이 어긋나면 바로 반환하거나 던지는 가드 절 형태가 읽기에도 편합니다.

자주 하는 실수

if (!name.trim())은 name이 null이거나 숫자일 때 함수가 아니라는 오류로 먼저 터집니다. 타입 검사가 값 검사보다 앞서야 합니다.

11Result 반환

성공이면 숫자, 실패면 null을 돌려주는 방식으로 결과와 실패를 하나의 반환값에 담았습니다. 예외 대신 값으로 오류를 다루고 싶을 때 이 모양을 씁니다. 다만 null은 실패 이유를 담지 못하고, parseNumber('0')의 결과 0이 if (!result) 검사에서 실패와 똑같이 취급되는 함정이 있습니다.

JavaScript code

// Result 반환
const parseNumber = (value) => {
  const parsed = Number(value);
  return Number.isNaN(parsed) ? null : parsed;
};
console.log(parseNumber("11"));
알아두면 좋은 점

실패를 값으로 돌려줄 때는 성공과 실패의 모양을 맞춰 두면, 이유를 담으면서도 호출부의 분기를 한 가지 형태로 통일할 수 있습니다.

자주 하는 실수

if (!parseNumber(input))로 실패를 판단하면 정상적으로 파싱된 0이 실패로 취급됩니다. === null로 명시해 비교해야 합니다.

12다단계 검증

검증 단계는 하나뿐이고, assert(true, ...) 호출은 조건이 참이라 아무 일 없이 지나갑니다. 여러 단계를 이으려면 assert를 순서대로 부르면 되는데, 첫 실패에서 즉시 멈추므로 뒤쪽 문제는 드러나지 않습니다. 오류를 한꺼번에 보여 주려면 던지는 대신 목록에 쌓았다가 마지막에 확인하는 방식이 필요합니다.

JavaScript code

// 다단계 검증
function assert(condition, message) {
  if (!condition) throw new Error(message);
}
assert(true, "must be true");
알아두면 좋은 점

검증 결과를 모아 보여 줄 때는 어느 필드에서 났는지 함께 담아야 합니다. 메시지만 나열하면 화면에서 입력란과 연결할 수 없습니다.

자주 하는 실수

여러 검사를 &&로 이어 붙이면 앞 조건이 거짓인 순간 뒤 조건은 평가되지 않습니다. 모든 오류를 모으려는 의도라면 이 단축 평가가 방해가 됩니다.

13로깅 래퍼

로깅 래퍼 함수는 없지만 catch가 오류를 삼키고 console.log로 메시지만 남기는 부분이 그 역할에 가깝습니다. 다시 던지지 않으면 호출한 쪽은 실패를 알 수 없다는 점을 함께 봐야 합니다. 기록을 남길 때는 message 문자열만이 아니라 오류 객체를 통째로 넘겨야 스택 추적이 보존됩니다.

JavaScript code

// 로깅 래퍼
try {
  JSON.parse("{");
} catch (error) {
  console.log(error instanceof Error ? error.message : String(error));
}
알아두면 좋은 점

오류 객체를 그대로 넘기면 콘솔이 스택까지 펼쳐 주지만, JSON으로 직렬화하면 message와 stack이 열거되지 않아 빈 객체가 됩니다.

자주 하는 실수

래퍼가 오류를 잡아 기록만 하고 다시 던지지 않으면 호출부는 실패를 성공으로 오인합니다. 로그는 남았는데 데이터는 비는 상황이 이렇게 만들어집니다.

14recover 흐름

복구 경로 없이, 조건이 어긋나면 던지고 정상이면 값을 반환하는 단방향 흐름입니다. 복구를 넣으려면 호출부에서 try/catch로 감싸 대체 이름을 채우면 됩니다. 던진 오류를 아무도 잡지 않으면 브라우저에서는 전역 error 이벤트로 흘러가고, Node에서는 프로세스가 그대로 종료됩니다.

JavaScript code

// recover 흐름
function requireName(name) {
  if (!name.trim()) throw new Error("name required");
  return name;
}
console.log(requireName("Ada"));
알아두면 좋은 점

복구 코드는 되도록 호출부에 두는 편이 낫습니다. 어떤 대체값이 적절한지는 실패한 함수보다 그 결과를 쓰는 쪽이 훨씬 잘 알고 있습니다.

자주 하는 실수

브라우저에서 처리되지 않은 오류는 화면 갱신만 중단시키고 페이지는 살아 있어, 사용자에게는 아무 반응 없이 멈춘 것처럼 보입니다.

15입력 가드

함수 입구에서 값을 숫자로 바꿔 보고 NaN이면 null을 내보내는 가드입니다. 폼 입력이나 쿼리 문자열처럼 값이 늘 문자열로 들어오는 경계에 이런 검사를 둡니다. Number('')와 Number(' ')는 0이 되어 이 가드를 그대로 통과하므로, 빈 문자열은 별도로 걸러야 합니다.

JavaScript code

// 입력 가드
const parseNumber = (value) => {
  const parsed = Number(value);
  return Number.isNaN(parsed) ? null : parsed;
};
console.log(parseNumber("15"));
알아두면 좋은 점

폼 값이나 쿼리 문자열은 항상 문자열이라 "0"도 참으로 평가됩니다. 숫자로 다룰 값은 경계에서 한 번만 변환하고 그 뒤로는 숫자로만 취급하는 편이 안전합니다.

자주 하는 실수

Number("")와 공백만 든 문자열이 모두 0이 되어 빈 입력이 0으로 저장됩니다. 변환 전에 공백을 지우고 빈 문자열을 걸러야 합니다.

정리하며

  • Error를 상속한 도메인 오류 클래스로 실패 종류를 구분 가능하게 만듭니다
  • finally에서 return이나 throw를 쓰면 원래 결과가 사라집니다
  • 원인 추적이 필요하면 cause 옵션으로 하위 오류를 연결합니다
  • parseInt는 부분 파싱을 허용하므로 Number나 정규식으로 검증합니다

더 깊이 들어가고 싶다면 JavaScript 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.