- Recognise the built-in error types (
TypeError,ReferenceError,RangeError,SyntaxError) and their usual causes - Create custom error classes, add a
causeand handle errors by type, including in async code - Read a stack trace and debug with breakpoints,
debuggerand console methods
A user writes: “The button does nothing.” There is no sign on the screen, but the console has a red line: in Chrome, TypeError: Cannot read properties of undefined (reading 'price'), followed by file names and numbers. For a beginner it is noise; for a professional it is a map pointing straight at the bug. In this lesson you will learn to read that map and to design your own errors well.
Built-in error types
| Type | Usual cause | Example |
|---|---|---|
TypeError | an operation on a value of the wrong type: calling a non-function, reading a property of undefined/null | null.name, (42)() |
ReferenceError | a variable that does not exist (or is in the TDZ) | console.log(missing) |
RangeError | a number outside the allowed range | new Array(-1) |
SyntaxError | code or JSON that cannot be parsed | JSON.parse('{bad}') |
const attempts = [
() => null.name,
() => missingVariable,
() => new Array(-1),
() => JSON.parse('{bad json}'),
() => (42)(),
];
for (const attempt of attempts) {
try {
attempt();
} catch (error) {
console.log(error.name, '|', error instanceof Error);
}
}▸ Expected output
TypeError | true ReferenceError | true RangeError | true SyntaxError | true TypeError | true
Your own errors and cause
throw can throw any value, but always throw an Error object — only it has a stack trace. A good rule: catch only the errors you can handle and rethrow the rest (throw error). To tell your program's errors apart, build a hierarchy of classes that extend Error — then instanceof lets you react to each kind properly.
class AppError extends Error {
constructor(message, options) {
super(message, options);
this.name = this.constructor.name;
}
}
class ValidationError extends AppError {}
class NotFoundError extends AppError {}
function findUser(users, id) {
if (!Number.isInteger(id)) throw new ValidationError(`Bad id: ${id}`);
const user = users.find((u) => u.id === id);
if (!user) throw new NotFoundError(`User ${id} not found`);
return user;
}
const users = [{ id: 1, name: 'Aysel' }];
for (const id of [1, 'x', 7]) {
try {
console.log(findUser(users, id).name);
} catch (error) {
if (error instanceof ValidationError) console.log('400', error.message);
else if (error instanceof NotFoundError) console.log('404', error.message);
else throw error;
}
}▸ Expected output
Aysel 400 Bad id: x 404 User 7 not found
this.constructor.name gives every subclass the right name automatically. 400 and 404 are HTTP status codes; we will come back to them in the API lesson.When you catch a low-level error and throw a clearer one, keep the original in the **cause** property (ES2022). This way the user sees a clear message and the developer does not lose the real reason.
function loadSettings(text) {
try {
return JSON.parse(text);
} catch (error) {
throw new Error('Settings file is broken', { cause: error });
}
}
try {
loadSettings('{ theme: dark }');
} catch (error) {
console.log(error.message);
console.log('Caused by:', error.cause.name);
console.log(error.cause instanceof SyntaxError);
}▸ Expected output
Settings file is broken Caused by: SyntaxError true
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function saveScore(score) {
await wait(100);
if (score < 0) throw new RangeError('Score cannot be negative');
return 'saved';
}
try {
console.log(await saveScore(10));
console.log(await saveScore(-5));
} catch (error) {
console.log(`${error.name}: ${error.message}`);
}
const result = await saveScore(-1).catch((error) => `fallback (${error.message})`);
console.log(result);▸ Expected output
saved RangeError: Score cannot be negative fallback (Score cannot be negative)
Reading a stack trace
A stack trace shows the chain of calls at the moment the error was created, newest on top. The first line is the error's name and message; the following lines look like at function (file:line:column). In the cart.js file below, the second item has no product property. This is the trace Node.js prints (folder paths shortened, Node's internal lines left out):
// cart.js
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.product.price * item.qty, 0);
}
function renderCart(cart) {
return calculateTotal(cart.items);
}
renderCart({ items: [{ product: { price: 2 }, qty: 1 }, { qty: 3 }] });TypeError: Cannot read properties of undefined (reading 'price')
at cart.js:3:57
at Array.reduce (<anonymous>)
at calculateTotal (cart.js:3:16)
at renderCart (cart.js:6:10)
at Object.<anonymous> (cart.js:8:1)Debugging tools
- 1Reproduce the bug
Find the exact steps that lead to the bug.
- 2Set a breakpoint
In DevTools ›
Sources, click a line number, or writedebugger;in the code — execution will stop there while DevTools is open. - 3Inspect the values
Look at the variables in
Scope, hover over values, add expressions toWatch. - 4Step through
Move line by line, step into a function or step out of it.
- 5Fix and verify
After the fix, repeat the same steps again.
The console has more useful methods than log. The runner on this page shows only console.log, so run this example in the browser console or in Node.js. The output below comes from Node.js; the time measured for sort differs on every run.
const students = [
{ name: 'Aysel', score: 92 },
{ name: 'Murad', score: 78 },
];
console.table(students);
console.group('Checkout');
console.log('items: 3');
console.warn('coupon expired');
console.groupEnd();
console.count('click');
console.count('click');
console.assert(students.length > 5, 'Expected more than 5 students');
console.time('sort');
students.sort((a, b) => b.score - a.score);
console.timeEnd('sort');┌─────────┬─────────┬───────┐ │ (index) │ name │ score │ ├─────────┼─────────┼───────┤ │ 0 │ 'Aysel' │ 92 │ │ 1 │ 'Murad' │ 78 │ └─────────┴─────────┴───────┘ Checkout items: 3 coupon expired click: 1 click: 2 Assertion failed: Expected more than 5 students sort: 0.027ms
Complete the ValidationError class (its name must be 'ValidationError') and write parseAge(text). If the text is not a whole number, throw a ValidationError with the message Not a whole number: <text>; if it is outside 0–120, use Out of range: <age>; otherwise return the age.
class ValidationError extends Error {
// set this.name in the constructor
}
function parseAge(text) {
// Number(text) must be an integer from 0 to 120
}
for (const input of ['16', 'abc', '130', '42']) {
try {
console.log('Age:', parseAge(input));
} catch (error) {
if (error instanceof ValidationError) {
console.log(`${error.name}: ${error.message}`);
} else {
throw error;
}
}
}▸ Expected output
Age: 16 ValidationError: Not a whole number: abc ValidationError: Out of range: 130 Age: 42
Write retry(fn, times): it calls fn at most times times until it succeeds and prints Retrying after: <message> after each failure. If every attempt fails, it throws an error with the message All <times> attempts failed that keeps the last error in cause.
function createFlakyTask(failuresBeforeSuccess) {
let calls = 0;
return () => {
calls++;
if (calls <= failuresBeforeSuccess) throw new Error(`Attempt ${calls} failed`);
return `Success on attempt ${calls}`;
};
}
function retry(fn, times) {
// your code here
}
console.log(retry(createFlakyTask(2), 3));
try {
retry(createFlakyTask(5), 3);
} catch (error) {
console.log(error.message, '| cause:', error.cause.message);
}▸ Expected output
Retrying after: Attempt 1 failed Retrying after: Attempt 2 failed Success on attempt 3 Retrying after: Attempt 1 failed Retrying after: Attempt 2 failed Retrying after: Attempt 3 failed All 3 attempts failed | cause: Attempt 3 failed
Key points
TypeErrormeans a wrong type orundefined,ReferenceErroran unknown variable,RangeErrora value out of range,SyntaxErrorcode or JSON that cannot be parsed.- Check
error.nameorinstanceof, not the message text — messages vary between browsers. - Custom error classes and
causemake errors meaningful and keep the original reason. - An async error is caught only with
awaitinsidetryor with.catch(). - Read a stack trace from the first line with your own file; use breakpoints,
debuggerand console methods instead of guessing.
Check yourself
10 questions. Every correct answer earns XP.
const user = null; user.length; throw?