Error Types & try/catch
Error Types & try/catch
Robust programs anticipate failure. JavaScript has a rich error hierarchy and a structured mechanism for handling exceptions.
Built-in Error Types
JavaScript ships with several specialised Error subclasses:
| Type | Trigger |
|---|---|
Error | Generic base class |
TypeError | Operation on the wrong type (e.g., calling a non-function) |
RangeError | Value outside an allowed range (e.g., invalid array length) |
ReferenceError | Accessing an undeclared variable |
SyntaxError | Malformed code (usually at parse time) |
URIError | Malformed URI in encodeURI / decodeURI |
EvalError | Deprecated; from eval() |
try / catch / finally
try {
// code that might throw
} catch (err) {
// handle the error; err is the thrown value
} finally {
// always runs — cleanup goes here
}finally runs regardless of whether an exception was thrown, even if a return statement was executed inside try or catch.
Re-throwing Errors
Only catch what you can handle. Re-throw anything you don't understand:
try {
riskyOp();
} catch (err) {
if (err instanceof NetworkError) {
handleNetworkError(err);
} else {
throw err; // propagate unexpected errors
}
}Error Properties
Every Error object has:
message— human-readable descriptionname— error type name ("TypeError", "RangeError", etc.)stack— stack trace string (non-standard but universal in practice)
Throwing Non-Error Values
You can technically throw anything (string, number, object), but always throw Error instances so that callers get a stack trace and can use instanceof.
Code Examples
function demonstrateErrors() {
const errors = [
() => null.property, // TypeError
() => undeclaredVar, // ReferenceError
() => new Array(-1), // RangeError
() => JSON.parse("{bad}"), // SyntaxError
];
errors.forEach((fn, i) => {
try {
fn();
} catch (err) {
console.log(`Error ${i + 1}: [${err.name}] ${err.message}`);
}
});
}
demonstrateErrors();Each built-in Error subclass signals a distinct category of failure. Checking err.name or using instanceof lets you handle each type differently.
function parseConfig(json) {
let parsed;
try {
parsed = JSON.parse(json);
if (typeof parsed !== "object" || parsed === null) {
throw new TypeError("Config must be an object");
}
if (!parsed.version) {
throw new RangeError("Config must have a version");
}
return parsed;
} catch (err) {
if (err instanceof SyntaxError) {
console.error("Invalid JSON:", err.message);
return null;
}
throw err; // re-throw TypeError, RangeError, etc.
} finally {
console.log("parseConfig attempt completed");
}
}
// Test cases
console.log(parseConfig('{"version": 2}'));
console.log(parseConfig("{invalid}"));
try {
parseConfig("null");
} catch (e) {
console.log("Caught re-thrown:", e.constructor.name, e.message);
}finally always runs, making it ideal for cleanup. Selective re-throwing lets higher-level code handle errors it understands, while unexpected errors bubble up naturally.
function inner() {
throw new RangeError("Value out of bounds: 150 not in [0, 100]");
}
function middle() {
inner();
}
function outer() {
try {
middle();
} catch (err) {
console.log("name: ", err.name);
console.log("message:", err.message);
// Stack traces vary by environment; just show first line
const firstLine = err.stack.split("\n")[0];
console.log("stack[0]:", firstLine);
// Check type
console.log("is RangeError:", err instanceof RangeError);
console.log("is Error: ", err instanceof Error);
}
}
outer();Every Error has name, message, and stack properties. The stack trace shows the call chain from the throw site. instanceof works hierarchically — RangeError is also an Error.
Quick Quiz
1. When does the 'finally' block execute?
2. Which error is thrown when you try to access a property on null?
3. What is the best practice when catching an error you cannot fully handle?
4. What three properties does every Error object have?
Was this lesson helpful?