에러는 타입을 가진 객체다
자바스크립트에서 에러가 발생하면 단순한 문자열이 튀어나오는 게 아니라, 하나의 객체가 던져진다. 이 객체에는 타입(생성자)이 있고, name, message, stack 같은 표준 속성들이 따라붙는다.
err.name은 "TypeError" 같은 짧은 식별자이고, err.message는 사람이 읽을 수 있는 설명입니다. 특정 클래스인지 확인하려면 err instanceof TypeError를 쓰면 됩니다. 이렇게 에러의 종류를 아는 게 왜 중요하냐면, 단순 오타인지, 값이 잘못됐는지, 아니면 아예 코드가 파싱조차 안 된 건지 바로 판단할 수 있기 때문입니다.
자바스크립트 에러 종류는 내장 타입만 총 7가지입니다. 이 중 3개는 개발하면서 지겹도록 보게 되고, 나머지 4개는 가끔씩 마주칩니다.
SyntaxError: 코드가 파싱조차 안 된 경우
SyntaxError는 자바스크립트 엔진이 코드를 읽어 들이지도 못했다는 뜻입니다. 엄밀히 말하면 런타임 에러가 아니라, 코드가 한 줄도 실행되기 전에 파싱 단계에서 실패한 것이죠. 같은 파일 안에서 try/catch로 잡는 것도 불가능합니다. 파일 전체가 통째로 거부되기 때문입니다.
function greet(name {
return "안녕하세요, " + name;
}
// SyntaxError: Unexpected token '{'
중괄호 빠뜨리거나, 쉼표 하나 잘못 들어가거나, 함수 밖에 return을 써놓거나 — 이렇게 문법이 깨지는 순간 바로 이 에러가 터집니다. 해결 방법은 단 하나, 소스 코드를 고치는 것뿐이에요. 그런데 딱 한 가지, 런타임에 파싱이 일어나는 상황에서는 SyntaxError를 try/catch로 잡을 수 있습니다. 대표적인 예가 JSON.parse죠:
JSON.parse는 런타임에 문자열을 받아 처리하기 때문에, 여기서 발생하는 문법 오류는 try/catch로 잡을 수 있습니다. 반면 여러분이 직접 작성한 소스 파일의 문법 오류는 잡을 수 없습니다.
ReferenceError: 존재하지 않는 이름을 참조할 때
ReferenceError는 현재 코드가 접근할 수 있는 어떤 스코프에도 선언되지 않은 변수를 참조할 때 발생합니다.
열 번 중 아홉 번은 단순 오타입니다(total을 totl로 적었다든가). 나머지 한 번은 스코프 문제인데, 다른 함수나 모듈에 선언된 걸 끌어다 쓰려고 할 때 발생하죠.
조금 더 미묘한 원인이 하나 더 있습니다. 바로 일시적 사각지대(Temporal Dead Zone, TDZ) 입니다. let과 const로 선언한 변수는 블록 최상단부터 존재하긴 하지만, 실제 선언문이 나오기 전까지는 접근할 수 없습니다:
console.log(x)가 실행되는 시점에 x는 이미 바인딩은 되어 있지만, 아직 초기화되지 않은 상태입니다. 그래서 참조 에러가 발생하는 거죠. 해결 방법은 간단합니다. 접근하는 코드를 선언문 아래로 옮기면 됩니다.
TypeError 자바스크립트: 값의 형태가 맞지 않을 때
TypeError는 값 자체는 존재하는데, 그 값이 해당 연산에서 기대하는 종류가 아닐 때 발생합니다. 함수가 아닌 걸 호출하려 하거나, null이나 undefined의 프로퍼티를 읽으려 하거나, const에 재할당하려 할 때 — 전부 TypeError에 해당하죠.
"Cannot read properties of null (reading 'name')" 은 자바스크립트에서 가장 자주 마주치는 에러 메시지라고 해도 과언이 아닙니다. 해결 방법은 두 가지인데, 값이 반드시 존재하도록 보장하거나, 옵셔널 체이닝으로 접근을 감싸주는 것입니다: user?.name.
이 외에도 자주 만나게 되는 TypeError 유형들은 다음과 같습니다:
숫자를 함수처럼 호출하거나, const로 선언한 값에 다시 할당하거나, 존재하지 않는 메서드를 호출하는 경우 — 요청한 동작에 비해 값의 타입이 맞지 않을 때 발생하는 에러입니다.
RangeError: 숫자가 허용 범위를 벗어났을 때
RangeError는 숫자 자체는 문법적으로 문제가 없지만, 특정 연산에서 허용하는 범위를 벗어났을 때 던져집니다.
대표적인 원인은 호출 스택을 터뜨리는 무한 재귀입니다:
"Maximum call stack size exceeded"는 거의 항상 함수가 종료 조건 없이 자기 자신을 호출하거나, 두 함수가 서로를 무한히 호출하는 상황에서 발생합니다.
URIError와 EvalError: 보기 드문 에러들
URIError는 encodeURI, decodeURIComponent 같은 URI 처리 함수에 잘못된 형식의 입력이 들어왔을 때 발생합니다:
EvalError는 사실상 유물이에요. 요즘 자바스크립트 엔진은 이 에러를 실제로 던지는 경우가 없고, 하위 호환성을 위해 생성자만 남아 있습니다. 수동으로 만들어볼 순 있지만, 실전에서 마주칠 일은 없다고 봐도 돼요.
상속 구조 살펴보기
앞서 살펴본 에러 타입들은 모두 기본 Error를 상속받습니다. 그래서 어떤 에러든 err instanceof Error는 true가 되죠. 아래처럼 범용 catch 블록에서 유용하게 써먹을 수 있습니다:
catch 블록은 말 그대로 전부 잡아냅니다. 누군가 throw "oops"처럼 문자열을 던져도 그대로 걸리죠. 마지막 분기가 중요한 이유가 바로 이겁니다. 잡힌 값을 에러 객체로 다루기 전에 항상 instanceof로 타입을 좁혀 주세요.
일부러 에러 던지기
코드에서 문제를 감지했을 때, 내장 에러 타입을 직접 던질 수도 있습니다. 상황에 맞는 타입을 골라 쓰면 됩니다.
에러 타입을 실패 상황에 맞게 고르는 건 단순히 보기 좋으라고 하는 일이 아닙니다. 타입을 제대로 맞춰두면, 이 함수를 호출하는 쪽에서 에러 메시지를 문자열로 파싱하는 대신 catch 블록에서 타입 기반으로 정확하게 분기 처리할 수 있습니다.
커스텀 에러 클래스 만들기
내장 에러 타입으로는 상황을 제대로 표현하기 어려울 때가 있는데, 이럴 때는 Error를 상속해서 직접 만들어 쓰면 됩니다.
꼭 두 가지만 기억하세요. 하나는 super(message)를 호출해서 부모 Error 클래스를 제대로 초기화해주는 것, 다른 하나는 this.name을 지정해서 로그에 에러 이름이 올바르게 찍히도록 하는 것입니다. field 같은 커스텀 속성을 두면, 호출하는 쪽에서 에러 메시지 문자열을 파싱하지 않고도 특정 실패 상황에 맞춰 대응할 수 있습니다.
다음 단계: 콘솔과 개발자 도구
에러 종류를 아는 건 절반일 뿐입니다. 나머지 절반은 스택 트레이스를 읽고, 프로그램이 돌아가는 도중에 상태를 직접 들여다보는 능력이죠. 브라우저 개발자 도구(그리고 Node의 디버거)를 쓰면 "에러는 났는데 왜 났는지 모르겠다"는 막막한 상황이 몇 초짜리 확인 작업으로 바뀝니다. 이제 그 이야기를 해보겠습니다.
자주 묻는 질문
자바스크립트에 내장된 에러 타입은 어떤 것들이 있나요?
총 7가지입니다. 베이스 클래스인 Error, 그리고 SyntaxError, ReferenceError, TypeError, RangeError, URIError, EvalError가 있습니다. 각각은 생성자 함수라서 호출하면 name, message, stack 프로퍼티를 가진 에러 객체를 만들어냅니다. 실무에서 가장 자주 만나게 되는 건 SyntaxError, ReferenceError, TypeError 세 가지예요.
SyntaxError와 TypeError는 뭐가 다른가요?
SyntaxError는 코드 자체가 문법적으로 잘못돼서 엔진이 파싱조차 못 하는 상태입니다. 반면 TypeError는 파싱은 정상적으로 끝났는데 런타임에 잘못된 동작을 했을 때 발생해요. 함수가 아닌 걸 호출했다든가, null의 프로퍼티를 읽으려 했다든가 하는 경우죠. 문법 에러는 스크립트 전체가 실행조차 안 되지만, 타입 에러는 해당 라인이 실행될 때에만 터집니다.
ReferenceError는 언제 발생하나요?
선언되지 않은 식별자를 사용하거나, let/const로 선언한 변수를 TDZ(일시적 사각지대, Temporal Dead Zone) 안에서 접근할 때 발생합니다. 가장 흔한 원인은 오타예요. 예를 들어 consoel.log(x)라고 치면 ReferenceError: consoel is not defined가 뜹니다. 이런 에러를 만나면 먼저 철자와 스코프부터 확인해 보세요.
에러 타입을 직접 만들어서 쓸 수도 있나요?
네, 가능합니다. 내장 Error 클래스를 상속받으면 돼요. class ValidationError extends Error { } 이런 식으로요. 이때 생성자에서 this.name을 지정해 줘야 로그나 catch 분기에서 다른 에러와 구분할 수 있습니다. 실패 케이스마다 처리 방식이 달라야 하는 프로젝트라면 커스텀 에러 클래스를 만들어 두는 게 훨씬 깔끔합니다.