Errors
Distinguish syntax, runtime, and logical errors, understand JavaScript's built-in Error types, and learn how to create custom error classes.
Things go wrong in every program: a typo breaks the code before it even runs, a function is called with the wrong kind of value, or the logic itself is simply incorrect even though nothing 'crashes.' Understanding the different categories of errors — and JavaScript's built-in Error types — is the foundation for writing code that fails predictably and can be debugged and handled gracefully.
Syntax errors
A syntax error occurs when code violates the grammatical rules of the JavaScript language itself — a missing closing brace, an unmatched parenthesis, a misspelled keyword. Syntax errors are caught before any code runs at all; the JavaScript engine cannot even begin executing a file that fails to parse.
Because they're caught at parse time, syntax errors can't be handled with try/catch — the broken code never reaches the point of execution where a catch block could intervene. They must be fixed in the source code itself.
Runtime errors
A runtime error occurs while the code is executing — the syntax was valid, the program started running, but then something went wrong partway through, like calling a method on undefined, or referencing a variable that was never declared.
Unlike syntax errors, runtime errors CAN be caught and handled using try/catch, because the program is actively executing when they occur — the JavaScript engine throws an Error object at that exact point, which a surrounding catch block can intercept.
Logical errors
A logical error is the trickiest kind: the code has valid syntax, runs without throwing any exception, but produces the wrong result because the programmer's logic itself is flawed — for example, using > instead of >= in a comparison, or an off-by-one mistake in a loop.
JavaScript has no way to automatically detect logical errors, since nothing is technically 'broken' from the language's point of view. Finding them requires testing, careful reasoning about expected output, and tools like debuggers or well-placed console.log statements.
Built-in Error types
JavaScript provides a base Error class and several specialized subclasses that the engine throws automatically in specific situations. Each carries a .message describing what went wrong and a .name identifying its type.
TypeError is thrown when a value is not of the expected type — for example, calling something that isn't a function, or trying to read a property off null or undefined. ReferenceError is thrown when code references a variable that doesn't exist in any accessible scope. RangeError is thrown when a value is outside its legal range, such as creating an array with a negative length or calling toFixed() with an invalid digit count.
• TypeError — wrong type of value used in an operation (null.property, notAFunction()) • ReferenceError — using a variable/identifier that has not been declared • RangeError — a numeric value is outside its allowed range • SyntaxError — invalid syntax, usually thrown by the parser or by JSON.parse() • Error — the generic base class all the above extend, usable directly for custom cases
Custom errors
You can define your own error types by extending the built-in Error class. This is useful for representing domain-specific failures (like 'InsufficientFundsError' or 'ValidationError') distinctly from generic errors, letting calling code check error.name or use instanceof to react differently to different failure kinds.
A custom error class typically calls super(message) in its constructor to set up the inherited .message property, then sets this.name to the class's own name so error.name and stack traces correctly identify the custom type instead of showing the generic 'Error'.
Defining and throwing a custom error
super(message) forwards the message to the built-in Error constructor, which sets this.message. Setting this.name after super() ensures error.name reports 'ValidationError' instead of the default 'Error'. throw new ValidationError(...) creates and throws an instance in one step.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
}
}
function setAge(age) {
if (age < 0) {
throw new ValidationError('Age cannot be negative');
}
return age;
}Code Examples
This example shows several common built-in error types in context (some lines are commented out since they would stop the script). The live try/catch block demonstrates that accessing a property on null throws a TypeError at runtime, which is then caught and its.name and.message inspected. Expected result: TypeError Cannot read properties of null (reading 'someProperty') null.someProperty; // throws TypeError
The first function throws a genuine runtime TypeError when given null, since null has no.reduce method — this is a runtime error that would be visible immediately as a thrown exception. The second function has a logical error: dividing by numbers.length - 1 instead of numbers.length. It runs without any error at all, but silently returns an incorrect average, which is much harder to notice without carefully checking the output. Expected result: 6 return total / (numbers.length - 1); // BUG
Common mistake: Logical errors are dangerous precisely because nothing crashes — the code appears to work, and the wrong result can go unnoticed until it causes a real problem downstream.
AppError is a shared base class for all custom application errors, automatically setting this.name to the actual subclass name using this.constructor.name. InsufficientFundsError extends it further, adding its own data (balance and amount) alongside a descriptive message. The catch block uses instanceof to specifically handle this known error type and extract its extra data, while re-throwing anything unexpected rather than silently swallowing it. Expected result: InsufficientFundsError: Cannot withdraw $100: balance is only $50 Shortfall: 50 if (error instanceof InsufficientFundsError) {... }
// TypeError: calling something that isn't a function
const greeting = 'hello';
// greeting(); // TypeError: greeting is not a function
// ReferenceError: variable was never declared
// console.log(undeclaredVariable); // ReferenceError: undeclaredVariable is not defined
// RangeError: invalid array length
// new Array(-1); // RangeError: Invalid array length
try {
null.someProperty;
} catch (error) {
console.log(error.name); // 'TypeError'
console.log(error.message); // "Cannot read properties of null (reading 'someProperty')"
}// Runtime error version: crashes
function average(numbers) {
return numbers.reduce((sum, n) => sum + n) / numbers.length;
}
// average(null); // TypeError: Cannot read properties of null (reading 'reduce')
// Logical error version: runs, but gives the WRONG answer silently
function averageBuggy(numbers) {
let total = 0;
for (let i = 0; i < numbers.length; i++) {
total += numbers[i];
}
return total / (numbers.length - 1); // BUG: should divide by numbers.length
}
console.log(averageBuggy([2, 4, 6])); // 6 (should be 4)class AppError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
}
}
class InsufficientFundsError extends AppError {
constructor(balance, amount) {
super(`Cannot withdraw $${amount}: balance is only $${balance}`);
this.balance = balance;
this.amount = amount;
}
}
function withdraw(balance, amount) {
if (amount > balance) {
throw new InsufficientFundsError(balance, amount);
}
return balance - amount;
}
try {
withdraw(50, 100);
} catch (error) {
if (error instanceof InsufficientFundsError) {
console.log(`${error.name}: ${error.message}`);
console.log('Shortfall:', error.amount - error.balance);
} else {
throw error; // re-throw anything we don't specifically handle
}
}Important Rules
Syntax errors are caught at parse time, before execution, and cannot be handled with try/catch.
Runtime errors occur during execution and can be caught with try/catch.
Logical errors produce no exception at all — the code runs but gives an incorrect result.
TypeError, ReferenceError, and RangeError are the most common built-in Error subclasses, each signaling a specific category of problem.
A custom error class should extend Error, call super(message), and set this.name so its type is identifiable.
Where this shows up
Well-designed applications define custom error types (like ValidationError, NotFoundError, or NetworkError) so that different parts of the codebase can catch and respond to specific failure categories differently — showing a form validation message versus a 'page not found' screen versus a retry prompt — instead of treating every failure identically.
class MyError extends Error { constructor(msg) { this.message = msg; } } class MyError extends Error { constructor(msg) { super(msg); this.name = 'MyError'; } }
Forgetting to call super(msg) before using 'this' in a derived class constructor throws a ReferenceError ('must call super before accessing this'), because the base Error class needs to initialize the instance first.
// Assuming any bug will throw an error and be visible immediately
Write tests and verify actual output values, not just 'does it run without crashing'.
Logical errors run perfectly fine without throwing anything, so relying on the absence of a thrown error as proof of correctness misses an entire category of bugs.
[Multiple Choice] Which type of error can be caught and handled with a try/catch block? A. Only syntax errors B. Only logical errors C. Runtime errors D. None — all errors must be fixed in the source code
C. Runtime errors — Runtime errors occur while the program is actively executing, which is exactly when a try/catch block is active and able to intercept the thrown Error object. Syntax errors happen before execution starts and cannot be caught this way.
[True / False] A logical error always causes JavaScript to throw an exception. A. True B. False
B. False — False. A logical error produces an incorrect result without throwing any exception at all — the code runs 'successfully' from the engine's point of view, which is exactly what makes logical errors hard to detect.
Create a custom error class OutOfStockError extending Error, whose constructor takes a product name and sets a message like 'PRODUCT is out of stock', with this.name set to 'OutOfStockError'. Write a function buyItem(product, stock) that throws it when stock is 0, and test it with a try/catch that logs error.name and error.message.
class OutOfStockError extends Error { constructor(product) { super(${product} is out of stock); this.name = 'OutOfStockError'; } } function buyItem(product, stock) { if (stock === 0) { throw new OutOfStockError(product); } return Purchased ${product}; } try { buyItem('Widget', 0); } catch (error) { console.log(error.name, '-', error.message); }
Remember to call super(message) before setting this.name, and check instanceof or error.name in the catch block.