Custom Errors & Debugging
Custom Errors & Debugging
Creating custom error classes makes error handling more semantic and powerful. The console object provides far more debugging tools than just console.log.
Extending Error
Create domain-specific error types by extending the built-in Error class:
class ValidationError extends Error {
constructor(message, field) {
super(message); // set this.message
this.name = "ValidationError";
this.field = field; // custom property
}
}Always call super(message) and set this.name manually — otherwise name stays as "Error".
Why Custom Errors?
instanceofchecks become meaningful:err instanceof ValidationError- Carry extra context (field, statusCode, requestId) alongside the message
- Catch blocks can discriminate between error types precisely
Error Hierarchy Pattern
class AppError extends Error { ... }
class DatabaseError extends AppError { ... }
class NotFoundError extends DatabaseError { ... }A single catch (err) { if (err instanceof AppError) ... } catches all domain errors.
Console Methods
Most developers use only console.log. Here is the full toolkit:
| Method | Purpose |
|---|---|
console.log | General output |
console.warn | Warning (yellow in DevTools) |
console.error | Error (red, includes stack in DevTools) |
console.table | Render arrays/objects as a table |
console.time / timeEnd | Measure elapsed time |
console.group / groupEnd | Collapsible log groups |
console.assert | Log only if condition is false |
console.dir | Inspect object properties interactively |
console.count | Count how many times a label is reached |
console.trace | Print a stack trace at the current position |
Code Examples
class AppError extends Error {
constructor(message, code) {
super(message);
this.name = "AppError";
this.code = code;
}
}
class ValidationError extends AppError {
constructor(message, field) {
super(message, "VALIDATION_ERROR");
this.name = "ValidationError";
this.field = field;
}
}
class NotFoundError extends AppError {
constructor(resource, id) {
super(`${resource} with id ${id} not found`, "NOT_FOUND");
this.name = "NotFoundError";
this.resource = resource;
}
}
function processRequest(data) {
if (!data.name) throw new ValidationError("Name is required", "name");
if (data.id === 0) throw new NotFoundError("User", 0);
return { ok: true };
}
const testCases = [
{ id: 1, name: "Alice" },
{ id: 1 }, // missing name
{ id: 0, name: "Bob" }, // not found
];
testCases.forEach((tc) => {
try {
console.log(JSON.stringify(processRequest(tc)));
} catch (err) {
if (err instanceof ValidationError) {
console.log(`Validation [${err.field}]: ${err.message}`);
} else if (err instanceof NotFoundError) {
console.log(`Not found [${err.resource}]: ${err.message}`);
} else {
throw err;
}
}
});Custom error classes carry structured data (field, resource, code) alongside the message. instanceof discrimination in catch blocks enables clean, type-safe error routing without inspecting message strings.
const users = [
{ id: 1, name: "Alice", role: "admin", score: 95 },
{ id: 2, name: "Bob", role: "user", score: 72 },
{ id: 3, name: "Carol", role: "user", score: 88 },
];
// console.table: ideal for arrays of objects
console.table(users);
// console.time / timeEnd: performance measurement
console.time("filter+map");
const adminNames = users
.filter((u) => u.role === "admin")
.map((u) => u.name);
console.timeEnd("filter+map");
console.log("Admins:", adminNames);
// console.group: collapsible sections
console.group("User Report");
users.forEach((u) => {
console.log(` ${u.name} (${u.role}): ${u.score}`);
});
console.groupEnd();
// console.assert: only logs when condition is false
console.assert(users.length > 0, "Expected at least one user");
console.assert(users.length > 10, "Expected more than 10 users — THIS LOGS");console.table gives a readable snapshot of tabular data. console.time/timeEnd measures micro-benchmarks. console.group organises related output. console.assert fires only when the condition is false — useful for invariant checks.
class HttpError extends Error {
constructor(status, message) {
super(message);
this.name = "HttpError";
this.status = status;
}
}
class AuthError extends HttpError {
constructor() { super(401, "Unauthorized — please log in"); }
}
class ForbiddenError extends HttpError {
constructor() { super(403, "Forbidden — insufficient permissions"); }
}
function handleError(err) {
if (err instanceof AuthError) console.log("[AUTH] ", err.message);
else if (err instanceof ForbiddenError) console.log("[FORBID] ", err.message);
else if (err instanceof HttpError) console.log(`[HTTP ${err.status}]`, err.message);
else console.log("[UNKNOWN]", err.message);
}
[
new AuthError(),
new ForbiddenError(),
new HttpError(404, "Not Found"),
new Error("Unexpected crash"),
].forEach(handleError);Hierarchical error classes let catch logic be both precise and broad. instanceof checks flow from specific to general, so AuthError (subclass) is caught before the broader HttpError check.
Quick Quiz
1. Why should you set this.name manually in a custom Error subclass?
2. What does console.assert(condition, message) do?
3. Which console method is best for displaying an array of objects as a formatted table?
Was this lesson helpful?