JavaScript Errors — Introduction, Silent Errors, Handling & the Error Object
Table of Contents
- Section 1 — Conceptual Understanding
- 1.1 What Is an Error?
- 1.2 The Two Worlds of Errors: Loud and Silent
- 1.3 Silent Errors — When JavaScript Says Nothing
- 1.4 Strict Mode — Making Silent Errors Loud
- 1.5 Loud Errors — try, catch, finally, and throw
- 1.6 The Error Object
- 1.7 The Six Built-In Error Types
- 1.8 Creating Custom Errors
- 1.9 Error Handling Patterns and Best Practices
- Section 2 — Applied Exercises
- Section 3 — Project Simulation
- Completion Checklist
Section 1 — Conceptual Understanding
1.1 What Is an Error?
The Real-World Analogy
Imagine you are a chef following a recipe. Halfway through, you reach for the salt — but the bottle is empty. You have a choice:
- Ignore it and serve unseasoned food (silent failure — the dish is wrong but nobody told the kitchen)
- Stop and report it so the kitchen manager can decide: get more salt, use a substitute, or cancel the dish (handled error — a decision is made)
- Crash the entire kitchen and serve no food to anyone (unhandled error — the whole program stops)
JavaScript programs face the same situation constantly. Something unexpected happens — a value is missing, a network request fails, a user types text where a number was expected — and the program must decide what to do.
Definition: An error in JavaScript is an unexpected event that disrupts the normal flow of a program. JavaScript represents errors as objects containing information about what went wrong, where, and why.
Why learning error handling matters
In a real job, bugs do not stop at development. Data arrives in unexpected shapes from APIs. Users behave unpredictably. Networks drop out. A well-written program anticipates these situations and handles them gracefully — showing the user a meaningful message instead of a blank screen or a cryptic console dump.
Error handling is the difference between software that fails loudly and helplessly, and software that fails safely and informedly.
1.2 The Two Worlds of Errors: Loud and Silent
JavaScript errors divide into two fundamental categories:
| Category | What Happens | JavaScript’s Reaction | Example |
|---|---|---|---|
| Loud Errors | Something clearly fails | Throws an error; stops execution at that line | Calling a method on undefined |
| Silent Errors | Something goes wrong but JavaScript keeps running | No error is thrown; wrong result produced | Assigning to a non-existent object property |
This distinction is critical because silent errors are the most dangerous — they do not crash your program, so you may not notice them until much later, when wrong data has already been stored in a database or sent to a user.
1.3 Silent Errors — When JavaScript Says Nothing
What is a silent error?
A silent error (sometimes called a “silent failure”) is a situation where JavaScript could tell you something is wrong but chooses not to — it simply continues running, producing incorrect or undefined behaviour without any warning.
Example 1: Writing to a read-only property
let PI = Math.PI; // PI = 3.141592653589793
PI = 99; // Attempt to reassign a read-only value
console.log(PI);
// Expected Output: 3.141592653589793
Nothing happened. No error. PI is still 3.141592653589793. The assignment PI = 99 was silently ignored.
💡 JavaScript simply did nothing. In strict mode, this would throw a
TypeError. In normal mode, it fails silently.
Example 2: Writing to a non-existent property of a primitive
let score = 42;
score.bonus = 10; // Numbers cannot have properties
console.log(score.bonus);
// Expected Output: undefined
No error is thrown. The property assignment is silently discarded. Reading it back returns undefined — but no warning was given.
Example 3: Deleting a non-configurable property
delete Math.PI; // Attempt to delete a core property
console.log(Math.PI);
// Expected Output: 3.141592653589793
Math.PI cannot be deleted. JavaScript ignores the delete and moves on.
Example 4: Duplicate parameter names
function greet(name, name) { // Two parameters with the same name!
console.log(name);
}
greet("Amara", "Kofi");
// Expected Output: Kofi (second value wins — first is silently overwritten)
No error. The function runs. The first name is silently replaced by the second. This is a common source of confusing bugs.
Example 5: Using an undeclared variable
function saveStudent() {
studentName = "Amara"; // No let, const, or var!
}
saveStudent();
console.log(studentName);
// Expected Output: Amara
JavaScript creates a global variable automatically. This is a serious silent error — you did not ask for a global variable, but now one exists, potentially contaminating other parts of your program.
Why silent errors are so dangerous
Imagine the following scenario at a bank:
function applyInterest(account) {
account.balance = account.Balance * 1.05; // typo: "Balance" vs "balance"
}
let account = { balance: 1000 };
applyInterest(account);
console.log(account.balance); // 1000 ← unchanged — NaN was silently discarded
account.Balance (capital B) is undefined. undefined * 1.05 is NaN. Assigning NaN to account.balance fails silently in some configurations, or worse — account.balance becomes NaN and no interest was applied. Your bank customer just lost their interest payment with zero warning.
1.4 Strict Mode — Making Silent Errors Loud
What is strict mode?
Strict mode is a setting you activate to make JavaScript stricter — it turns many silent errors into loud, thrown errors that you can detect and fix.
How to enable strict mode
Write "use strict"; as the very first statement in a script or a function:
"use strict"; // Enable for the entire script
// Or inside a function only:
function strictFunction() {
"use strict";
// strict mode applies only inside here
}
What strict mode catches
Let’s revisit the silent errors from above — now with strict mode:
"use strict";
// 1. Undeclared variable → now throws ReferenceError
function saveStudent() {
studentName = "Amara"; // ❌ ReferenceError: studentName is not defined
}
// 2. Writing to a read-only property → now throws TypeError
let PI = Math.PI;
PI = 99; // ❌ TypeError: Assignment to constant variable
// 3. Duplicate parameter names → now throws SyntaxError
function greet(name, name) { // ❌ SyntaxError: Duplicate parameter name
console.log(name);
}
// 4. Deleting an undeletable property → now throws TypeError
delete Math.PI; // ❌ TypeError: Cannot delete property 'PI' of #<Object>
Complete list of what strict mode prevents
| Silent Error | Without Strict | With Strict |
|---|---|---|
| Undeclared variable | Creates global | ReferenceError |
| Write to read-only property | Silently ignored | TypeError |
| Write to non-writable property | Silently ignored | TypeError |
| Duplicate parameter names | Second wins silently | SyntaxError |
| Delete non-configurable property | Silently ignored | TypeError |
Using with statement |
Allowed | SyntaxError |
this in function (non-method) |
Refers to window |
undefined |
💡 Best Practice: Always use
"use strict";at the top of your scripts. Modern JavaScript modules (import/export) have strict mode on by default. All major style guides (Airbnb, Google, StandardJS) require strict mode.
1.5 Loud Errors — try, catch, finally, and throw
The big idea
When JavaScript encounters a loud error (called an exception), it immediately stops running the current block of code and throws the error — it looks for a handler. If no handler is found, the program crashes.
The try...catch structure lets you handle that thrown error gracefully instead of crashing.
try — the risky zone
The try block contains code that might throw an error. JavaScript runs this code normally. If an error occurs anywhere inside, JavaScript immediately jumps to the catch block.
try {
// Code that might go wrong
}
catch — the safety net
The catch block runs only if an error was thrown in the try block. It receives the error object as a parameter.
catch (error) {
// Handle the error here
// error.message tells you what went wrong
}
finally — the cleanup crew
The finally block runs always — whether an error occurred or not. It is used for cleanup: closing files, stopping loading spinners, resetting state.
finally {
// Always runs — error or no error
}
Full Structure
try {
// Attempt something risky
} catch (error) {
// Handle the error if it happens
} finally {
// Always runs regardless
}
Micro-Demo 1: Basic try/catch
try {
let result = null;
console.log(result.toUpperCase()); // ❌ result is null — can't call method on null
} catch (error) {
console.log("Something went wrong: " + error.message);
}
// Expected Output: Something went wrong: Cannot read properties of null (reading 'toUpperCase')
Without try/catch, this would crash the program. With it, we capture the error and continue.
Micro-Demo 2: try/catch/finally
function loadData() {
console.log("Loading started...");
try {
let data = JSON.parse("{ invalid json }"); // ❌ bad JSON
console.log("Data loaded:", data);
} catch (error) {
console.log("Failed to parse data: " + error.message);
} finally {
console.log("Loading finished."); // always runs
}
}
loadData();
Expected Output:
Loading started...
Failed to parse data: Unexpected token i in JSON at position 2
Loading finished.
💡
finallyis critical for cleaning up resources. Even if the catch block throws another error,finallystill runs.
Micro-Demo 3: catch is optional, finally is optional (but not both)
// try with only finally (no catch) — error still propagates but finally runs
try {
undeclaredVariable;
} finally {
console.log("Cleanup ran."); // runs before the error propagates
}
// Output: Cleanup ran.
// Then: ReferenceError: undeclaredVariable is not defined
throw — creating your own errors
The throw statement lets you deliberately create and throw an error — on your own terms, based on your own business logic.
throw expression;
You can throw any value, but the best practice is to throw an Error object (covered in 1.6):
throw new Error("Something went wrong");
throw new TypeError("Expected a number");
throw new RangeError("Value must be between 0 and 100");
Micro-Demo 4: throw with custom validation
function setAge(age) {
if (typeof age !== "number") {
throw new TypeError("Age must be a number. Got: " + typeof age);
}
if (age < 0 || age > 150) {
throw new RangeError("Age must be between 0 and 150. Got: " + age);
}
console.log("Age set to: " + age);
}
try {
setAge("twenty");
} catch (error) {
console.log(error.name + ": " + error.message);
}
try {
setAge(-5);
} catch (error) {
console.log(error.name + ": " + error.message);
}
try {
setAge(25);
} catch (error) {
console.log(error.name + ": " + error.message);
}
Expected Output:
TypeError: Age must be a number. Got: string
RangeError: Age must be between 0 and 150. Got: -5
Age set to: 25
Re-throwing errors
Sometimes you catch an error, check if it is the kind you can handle, and if not — rethrow it for a higher-level handler:
function processInput(input) {
try {
if (typeof input !== "number") {
throw new TypeError("Input must be a number");
}
return input * 2;
} catch (error) {
if (error instanceof TypeError) {
console.log("Handled TypeError: " + error.message);
return 0; // provide a default
}
throw error; // rethrow anything we didn't expect
}
}
console.log(processInput("hello")); // Handled TypeError: Input must be a number → 0
console.log(processInput(5)); // 10
Expected Output:
Handled TypeError: Input must be a number
0
10
⚠️ Common Beginner Mistake — Swallowing errors silently
// ❌ Bad — this hides the error completely
try {
riskyOperation();
} catch (error) {
// empty catch — the error disappears!
}
// ✅ Better — always log or handle meaningfully
try {
riskyOperation();
} catch (error) {
console.error("riskyOperation failed:", error.message);
}
An empty catch block is one of the most dangerous patterns in JavaScript. The error is swallowed, the program continues with wrong state, and you have no idea anything went wrong.
1.6 The Error Object
What is the Error object?
When JavaScript throws an error, it creates an Error object — a structured package of information about the failure. The catch block receives this object.
The core properties
| Property | Type | What it contains |
|---|---|---|
name |
String | The type of error: "TypeError", "RangeError", etc. |
message |
String | A human-readable description of what went wrong |
stack |
String | A trace of which functions were called before the error |
Micro-Demo: Inspecting an Error object
try {
null.toString();
} catch (error) {
console.log("name: " + error.name);
console.log("message: " + error.message);
console.log("stack:\n" + error.stack);
}
Expected Output:
name: TypeError
message: Cannot read properties of null (reading 'toString')
stack:
TypeError: Cannot read properties of null (reading 'toString')
at <anonymous>:2:8
at ...
The stack trace — your debugging map
The stack property shows the call stack — the sequence of function calls that led to the error. Reading it from bottom to top shows you the path JavaScript took to reach the error:
function c() { null.toString(); }
function b() { c(); }
function a() { b(); }
try {
a();
} catch (error) {
console.log(error.stack);
}
Expected Output (simplified):
TypeError: Cannot read properties of null (reading 'toString')
at c (<anonymous>:1:14) ← error happened here
at b (<anonymous>:2:14) ← b called c
at a (<anonymous>:3:14) ← a called b
at <anonymous>:6:3 ← top-level called a
💡 Reading stack traces is one of the most valuable debugging skills. In a real job, when a bug report comes in, the stack trace is your first clue about where to look.
Creating an Error object manually
You can create Error objects using the new keyword without throwing them:
let myError = new Error("Something failed");
console.log(myError.name); // "Error"
console.log(myError.message); // "Something failed"
console.log(typeof myError); // "object"
You can also create specific error types (see Section 1.7):
let typeErr = new TypeError("Expected a string");
let rangeErr = new RangeError("Value out of bounds");
1.7 The Six Built-In Error Types
JavaScript has six specific error types, each representing a different category of failure. They all extend the base Error type, so they all have name, message, and stack.
1. RangeError
Thrown when a value is not within the allowed range of values.
// Array with negative length
try {
let arr = new Array(-1);
} catch (e) {
console.log(e.name + ": " + e.message);
// RangeError: Invalid array length
}
// toFixed with invalid decimal count
try {
let num = 3.14;
num.toFixed(200); // max allowed is 100
} catch (e) {
console.log(e.name + ": " + e.message);
// RangeError: toFixed() digits argument must be between 0 and 100
}
💡 Real-World Use: Validation of ages, percentages, scores — any numeric value with defined boundaries.
2. ReferenceError
Thrown when code refers to a variable that does not exist in the current scope.
try {
console.log(studentName); // never declared
} catch (e) {
console.log(e.name + ": " + e.message);
// ReferenceError: studentName is not defined
}
"use strict";
try {
undeclaredVar = 10; // no let/const/var
} catch (e) {
console.log(e.name + ": " + e.message);
// ReferenceError: undeclaredVar is not defined
}
💡 Real-World Use: This is extremely common. Typos in variable names, using a variable before it is declared, or accessing something outside its scope all produce
ReferenceError.
3. SyntaxError
Thrown when JavaScript cannot parse the code — the grammar rules of the language are broken. This usually happens at parse time, before the program even runs.
try {
eval("let x = ;"); // invalid JavaScript syntax
} catch (e) {
console.log(e.name + ": " + e.message);
// SyntaxError: Unexpected token ';'
}
try {
JSON.parse("{ name: Amara }"); // invalid JSON — keys must be quoted
} catch (e) {
console.log(e.name + ": " + e.message);
// SyntaxError: Unexpected token n in JSON at position 2
}
💡 Most
SyntaxErrors in your own code are caught by the browser/Node before execution. ButJSON.parse()throwsSyntaxErrorat runtime, which is very commonly caught in real programs.
4. TypeError
Thrown when an operation is performed on a value of the wrong type.
// Calling a non-function
try {
let x = 42;
x(); // 42 is not callable
} catch (e) {
console.log(e.name + ": " + e.message);
// TypeError: x is not a function
}
// Accessing property of null or undefined
try {
let user = null;
console.log(user.name);
} catch (e) {
console.log(e.name + ": " + e.message);
// TypeError: Cannot read properties of null (reading 'name')
}
// Wrong argument type
try {
"use strict";
Object.defineProperty({}, "x", { value: 1, writable: false });
// Trying to write to it:
let obj = {};
Object.defineProperty(obj, "y", { value: 42, writable: false });
obj.y = 99; // TypeError in strict mode
} catch (e) {
console.log(e.name + ": " + e.message);
}
💡 Most common error in production JavaScript. Accessing
.name,.length, or calling a method onnullorundefinedis the #1 cause of TypeError crashes in real apps. Always check that a value exists before accessing its properties.
5. URIError
Thrown when encodeURI() or decodeURI() are passed malformed URI sequences.
try {
decodeURIComponent("%"); // incomplete percent-encoding
} catch (e) {
console.log(e.name + ": " + e.message);
// URIError: URI malformed
}
💡 Real-World Use: When working with URLs from external sources (APIs, user input), always wrap
decodeURIComponent()in try/catch.
6. EvalError
This was thrown historically for errors in eval(). Modern JavaScript engines no longer throw it in practice, but the type still exists for legacy compatibility. You are unlikely to encounter it in real code.
let evalErr = new EvalError("eval is disabled here");
console.log(evalErr.name); // "EvalError"
console.log(evalErr.message); // "eval is disabled here"
Quick Reference — Which Error for Which Situation?
| Situation | Error Type |
|---|---|
| Value outside allowed range | RangeError |
| Variable not declared / not in scope | ReferenceError |
| Invalid code syntax / bad JSON | SyntaxError |
| Wrong type used / null property access | TypeError |
| Malformed URI string | URIError |
| Generic / custom errors | Error |
1.8 Creating Custom Errors
Why create custom errors?
Built-in error types describe technical failures. In real applications, you also need to describe business logic failures:
- “User not found” — not a
TypeError, not aRangeError— it is a domain-specific failure - “Insufficient funds” — meaningful to the application but not to JavaScript
- “Session expired” — needs special handling in the UI
Custom errors let you create named, meaningful error types for your application’s domain.
Method 1: Extending the Error class
class ValidationError extends Error {
constructor(message, field) {
super(message); // pass message to parent Error
this.name = "ValidationError";
this.field = field; // add extra context
}
}
class NetworkError extends Error {
constructor(message, statusCode) {
super(message);
this.name = "NetworkError";
this.statusCode = statusCode;
}
}
Using custom errors
function validateEmail(email) {
if (typeof email !== "string") {
throw new ValidationError("Email must be a string", "email");
}
if (!email.includes("@")) {
throw new ValidationError("Email must contain @", "email");
}
return true;
}
try {
validateEmail("amara-at-school.com");
} catch (error) {
if (error instanceof ValidationError) {
console.log("Validation failed on field: " + error.field);
console.log("Reason: " + error.message);
} else {
throw error; // rethrow unexpected errors
}
}
Expected Output:
Validation failed on field: email
Reason: Email must contain @
Using instanceof to identify error types
instanceof lets you check what kind of error you caught:
try {
riskyOperation();
} catch (error) {
if (error instanceof TypeError) {
console.log("Type problem:", error.message);
} else if (error instanceof RangeError) {
console.log("Range problem:", error.message);
} else if (error instanceof ValidationError) {
console.log("Validation problem:", error.message);
} else {
console.log("Unknown error:", error);
throw error; // always rethrow what you can't handle
}
}
1.9 Error Handling Patterns and Best Practices
Pattern 1: Guard clauses — validate early, throw early
Catch problems at the entrance of a function before doing any work:
function processPayment(amount, currency) {
// Guard clauses — check inputs first
if (typeof amount !== "number") throw new TypeError("Amount must be a number");
if (amount <= 0) throw new RangeError("Amount must be positive");
if (typeof currency !== "string") throw new TypeError("Currency must be a string");
if (currency.length !== 3) throw new RangeError("Currency must be a 3-letter code");
// Main logic — only reached if all guards pass
console.log(`Processing ${currency} ${amount.toFixed(2)}`);
}
try {
processPayment(100.50, "GHS");
processPayment(-50, "USD");
} catch (error) {
console.log(error.name + ": " + error.message);
}
Expected Output:
Processing GHS 100.50
RangeError: Amount must be positive
Pattern 2: Wrapping async operations
In real applications, many operations are asynchronous (network requests, file reads). Errors in async code must be handled with .catch() or try/catch inside async/await:
// With async/await (preview — you'll learn this fully in async tutorials)
async function fetchStudent(id) {
try {
let response = await fetch("https://api.school.com/students/" + id);
if (!response.ok) {
throw new NetworkError("Student not found", response.status);
}
let data = await response.json();
return data;
} catch (error) {
if (error instanceof NetworkError) {
console.log("Network issue (" + error.statusCode + "): " + error.message);
} else {
console.log("Unexpected error: " + error.message);
}
return null;
}
}
Pattern 3: Always log enough context
// ❌ Vague — useless in production
catch (error) {
console.log("Error occurred");
}
// ✅ Informative — helps debugging
catch (error) {
console.error(`[processPayment] Failed for amount=${amount}: ${error.name}: ${error.message}`);
console.error(error.stack);
}
Pattern 4: Never catch what you cannot handle
// ❌ Dangerous — catches everything, even programmer mistakes
try {
doSomething();
} catch (error) {
console.log("error happened");
// doesn't rethrow — error disappears
}
// ✅ Selective — catch known errors, rethrow the rest
try {
doSomething();
} catch (error) {
if (error instanceof ValidationError) {
showUserMessage(error.message);
} else {
throw error; // programmer errors should propagate
}
}
⚠️ Summary of Common Beginner Mistakes
| Mistake | Problem | Fix |
|---|---|---|
Empty catch block |
Error disappears silently | Always log or handle |
| Catching too broadly | Masks real bugs | Use instanceof to be specific |
| Not rethrowing | Unknown errors get hidden | Rethrow what you can’t handle |
| Ignoring silent errors | Wrong data silently propagates | Use "use strict" always |
Not using finally |
Resources left open on error | Use finally for cleanup |
| Throwing plain strings | No stack trace, no type info | Always throw new Error(...) |
Section 2 — Applied Exercises
Exercise 1 — Form Input Validator
Warm-Up Mini-Example
function checkInput(value, fieldName) {
try {
if (!value) throw new Error(fieldName + " cannot be empty");
console.log(fieldName + " is valid: " + value);
} catch (error) {
console.log("❌ " + error.message);
}
}
checkInput("Amara", "Name"); // Name is valid: Amara
checkInput("", "Email"); // ❌ Email cannot be empty
Objective
Build a complete form validator that uses custom error types, try/catch, and strict mode to validate a user registration form.
Scenario
You are building a student registration portal. The server receives form data and must validate it before saving to the database. Invalid data must be rejected with clear error messages.
Requirements
The form has these fields:
username— string, 3–20 characters, letters and numbers onlyemail— string, must contain@and.age— number or numeric string, must be between 16 and 100password— string, at least 8 characters
Step-by-Step Instructions
Step 1: Enable strict mode at the top.
Step 2: Create a ValidationError class with field and message properties.
Step 3: Write individual validator functions that throw ValidationError when a field is invalid.
Step 4: Write a validateForm(data) function that calls all validators in a try/catch block.
Step 5: Test with both valid and invalid data.
"use strict";
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
function validateUsername(username) {
if (typeof username !== "string") {
throw new ValidationError("Username must be a string", "username");
}
if (username.length < 3 || username.length > 20) {
throw new ValidationError("Username must be 3–20 characters", "username");
}
if (!/^[a-zA-Z0-9]+$/.test(username)) {
throw new ValidationError("Username can only contain letters and numbers", "username");
}
}
function validateEmail(email) {
if (typeof email !== "string") {
throw new ValidationError("Email must be a string", "email");
}
if (!email.includes("@") || !email.includes(".")) {
throw new ValidationError("Email must be a valid format (contains @ and .)", "email");
}
}
function validateAge(age) {
let numAge = Number(age);
if (isNaN(numAge)) {
throw new ValidationError("Age must be a number", "age");
}
if (numAge < 16 || numAge > 100) {
throw new RangeError("Age must be between 16 and 100");
}
}
function validatePassword(password) {
if (typeof password !== "string") {
throw new ValidationError("Password must be a string", "password");
}
if (password.length < 8) {
throw new ValidationError("Password must be at least 8 characters", "password");
}
}
function validateForm(data) {
console.log("Validating form for: " + data.username);
try {
validateUsername(data.username);
validateEmail(data.email);
validateAge(data.age);
validatePassword(data.password);
console.log("✅ All fields valid. Registration successful.\n");
} catch (error) {
if (error instanceof ValidationError) {
console.log(`❌ Validation error on '${error.field}': ${error.message}\n`);
} else if (error instanceof RangeError) {
console.log("❌ Range error: " + error.message + "\n");
} else {
console.log("❌ Unexpected error: " + error.message + "\n");
throw error;
}
}
}
// Test 1: Valid registration
validateForm({
username: "amara99",
email: "amara@school.com",
age: 17,
password: "securePass1"
});
// Test 2: Username too short
validateForm({
username: "am",
email: "amara@school.com",
age: 17,
password: "securePass1"
});
// Test 3: Invalid email
validateForm({
username: "kofi2024",
email: "kofi-school",
age: 20,
password: "mypassword"
});
// Test 4: Age out of range
validateForm({
username: "ama2024",
email: "ama@school.com",
age: 12,
password: "mypassword"
});
Expected Output:
Validating form for: amara99
✅ All fields valid. Registration successful.
Validating form for: am
❌ Validation error on 'username': Username must be 3–20 characters
Validating form for: kofi2024
❌ Validation error on 'email': Email must be a valid format (contains @ and .)
Validating form for: ama2024
❌ Range error: Age must be between 16 and 100
Self-Check Questions
- Why does
validateFormstop at the first error instead of collecting all errors? How would you modify it to collect all validation errors before reporting? validateAgecan receive a string and convert it — why is this useful in a real form?- What would happen if we caught
Errorinstead ofValidationErrorandRangeErrorseparately? Would we lose any information? - Why do we
throw errorat the end of theelsebranch invalidateForm?
What-If Challenge
Modify
validateFormto collect all errors instead of stopping at the first one. Return an array of error messages. (Hint: remove thetry/catchfrom inside the function and wrap each validator call in its owntry/catch.)
Exercise 2 — Safe JSON Parser
Warm-Up Mini-Example
function safeJSON(str) {
try {
return JSON.parse(str);
} catch (e) {
return null;
}
}
console.log(safeJSON('{"name":"Amara"}')); // { name: 'Amara' }
console.log(safeJSON("not valid json")); // null
Objective
Build a robust data importer that reads JSON strings from multiple sources, handles malformed data gracefully, and reports what was successfully imported vs. what failed.
Scenario
You work at a school that receives student data exports from three different systems. Each system produces JSON, but some files are occasionally malformed. Your importer must process all files and report results — without crashing on bad data.
"use strict";
let dataFiles = [
{ source: "SystemA", data: '{"name":"Amara Osei","age":17,"gpa":3.85}' },
{ source: "SystemB", data: '{"name":"Kofi Mensah","age":20,"gpa":3.1}' },
{ source: "SystemC", data: '{name:"Ama Darko",age:18}' }, // invalid JSON
{ source: "SystemD", data: '{"name":"Kweku","age":"twenty"}' }, // age is a string
{ source: "SystemE", data: "" } // empty
];
let imported = [];
let failed = [];
dataFiles.forEach(function(file) {
try {
if (!file.data || file.data.trim() === "") {
throw new Error("File is empty");
}
let parsed = JSON.parse(file.data); // SyntaxError if malformed
// Validate age field
let age = Number(parsed.age);
if (isNaN(age)) {
throw new TypeError("Age is not a valid number: " + parsed.age);
}
imported.push({ source: file.source, student: parsed });
console.log(`✅ ${file.source}: Imported ${parsed.name}`);
} catch (error) {
failed.push({ source: file.source, error: error.name + ": " + error.message });
console.log(`❌ ${file.source}: Failed — ${error.name}: ${error.message}`);
}
});
console.log("\n--- Import Summary ---");
console.log("Successfully imported: " + imported.length);
console.log("Failed: " + failed.length);
failed.forEach(f => console.log(" " + f.source + ": " + f.error));
Expected Output:
✅ SystemA: Imported Amara Osei
✅ SystemB: Imported Kofi Mensah
❌ SystemC: Failed — SyntaxError: Unexpected token n in JSON at position 1
❌ SystemD: Failed — TypeError: Age is not a valid number: twenty
❌ SystemE: Failed — Error: File is empty
--- Import Summary ---
Successfully imported: 2
Failed: 3
SystemC: SyntaxError: Unexpected token n in JSON at position 1
SystemD: TypeError: Age is not a valid number: twenty
SystemE: Error: File is empty
Self-Check Questions
- Why is it important to keep the
forEachloop running even after acatch? What would happen if we used a regularforloop and did not catch the error? - The
SyntaxErrorfromJSON.parse()and theTypeErrorwe throw manually are both caught by the samecatchblock. How could you handle them differently? SystemDhasage: "twenty"— what isNumber("twenty")? Why doesisNaNcatch this?
Exercise 3 — Stack Trace Reader
Objective
Build a function call chain, deliberately introduce an error deep in the chain, catch it at the top level, and read and interpret the stack trace.
Scenario
Understanding stack traces is a critical skill for debugging in a real job. This exercise builds that skill deliberately.
"use strict";
function calculateFinalGrade(scores) {
return computeAverage(scores);
}
function computeAverage(scores) {
return sumScores(scores) / scores.length;
}
function sumScores(scores) {
if (!Array.isArray(scores)) {
throw new TypeError("scores must be an array. Got: " + typeof scores);
}
return scores.reduce((total, score) => {
if (typeof score !== "number") {
throw new TypeError("Each score must be a number. Got: " + typeof score + " (" + score + ")");
}
return total + score;
}, 0);
}
// Test 1: Valid
try {
let grade = calculateFinalGrade([85, 90, 78]);
console.log("Final grade: " + grade.toFixed(1));
} catch (e) {
console.log(e.name + ": " + e.message);
}
// Test 2: Non-array input
try {
calculateFinalGrade("85, 90, 78");
} catch (e) {
console.log("\n" + e.name + ": " + e.message);
console.log("\nStack trace:");
console.log(e.stack);
}
// Test 3: Array with non-number
try {
calculateFinalGrade([85, "ninety", 78]);
} catch (e) {
console.log("\n" + e.name + ": " + e.message);
}
Expected Output:
Final grade: 84.3
TypeError: scores must be an array. Got: string
Stack trace:
TypeError: scores must be an array. Got: string
at sumScores (<anonymous>:9:11)
at computeAverage (<anonymous>:5:10)
at calculateFinalGrade (<anonymous>:1:10)
at <anonymous>:22:3
TypeError: Each score must be a number. Got: string (ninety)
Self-Check Questions
- Reading the stack trace for Test 2 from bottom to top — what was the order of function calls?
- The error was thrown in
sumScoresbut caught in thetryblock that calledcalculateFinalGrade. How did the error “travel” up through the call stack? - Why is knowing which function threw the error (from the stack trace) more useful than just knowing the error message?
Section 3 — Project Simulation
Mini-Project: Bank Transaction Processor
Project Overview
You will build a Bank Transaction Processor — a JavaScript program that:
- Manages a simple bank account with a balance
- Processes a batch of transactions (deposits and withdrawals)
- Uses custom errors, strict mode, try/catch/finally, and meaningful error reporting
- Produces a full transaction log with success/failure status for each transaction
Real-World Context
Banking systems must never crash silently. Every failed transaction must be logged with a reason. A programmer’s error (like passing the wrong type) must be distinguishable from a business-logic error (like insufficient funds). This project simulates that discipline.
Stage 1 — Setup: Account and Custom Errors
Illustrative Example First:
// Understand the structure before building
let account = { id: "ACC001", owner: "Amara", balance: 500 };
// A transaction attempt
function withdraw(account, amount) {
if (amount > account.balance) {
throw new Error("Insufficient funds");
}
account.balance -= amount;
}
try {
withdraw(account, 600);
} catch (e) {
console.log("Transaction failed: " + e.message);
// Transaction failed: Insufficient funds
}
Stage 1 Code:
"use strict";
// Custom error types
class InsufficientFundsError extends Error {
constructor(requested, available) {
super(`Cannot withdraw ${requested}. Available balance: ${available}`);
this.name = "InsufficientFundsError";
this.requested = requested;
this.available = available;
}
}
class InvalidTransactionError extends Error {
constructor(message) {
super(message);
this.name = "InvalidTransactionError";
}
}
// Account object
let account = {
id: "ACC-2024-001",
owner: "Amara Osei",
balance: 1000.00,
log: []
};
console.log("Account created for: " + account.owner);
console.log("Opening balance: $" + account.balance.toFixed(2));
Expected Stage 1 Output:
Account created for: Amara Osei
Opening balance: $1000.00
Stage 2 — Adding Features: deposit, withdraw, and processTransactions
Illustrative Example First:
// Understand finally before the full stage
function attemptWithdrawal(amount) {
console.log("Attempting withdrawal of $" + amount);
try {
if (amount > 500) throw new Error("Too large");
console.log("Withdrawal successful");
} catch (e) {
console.log("Failed: " + e.message);
} finally {
console.log("Transaction attempt recorded.\n");
}
}
attemptWithdrawal(200); // Withdrawal successful → Transaction attempt recorded.
attemptWithdrawal(900); // Failed: Too large → Transaction attempt recorded.
Stage 2 Code:
function deposit(account, amount) {
if (typeof amount !== "number" || isNaN(amount)) {
throw new InvalidTransactionError("Deposit amount must be a valid number. Got: " + amount);
}
if (amount <= 0) {
throw new InvalidTransactionError("Deposit amount must be positive. Got: " + amount);
}
account.balance += amount;
return amount;
}
function withdraw(account, amount) {
if (typeof amount !== "number" || isNaN(amount)) {
throw new InvalidTransactionError("Withdrawal amount must be a valid number. Got: " + amount);
}
if (amount <= 0) {
throw new InvalidTransactionError("Withdrawal amount must be positive. Got: " + amount);
}
if (amount > account.balance) {
throw new InsufficientFundsError(amount, account.balance);
}
account.balance -= amount;
return amount;
}
function processTransactions(account, transactions) {
console.log("\n--- Processing Transactions ---\n");
transactions.forEach(function(tx, index) {
let txNum = String(index + 1).padStart(2, "0");
let success = false;
let note = "";
try {
if (tx.type === "deposit") {
let amount = deposit(account, tx.amount);
note = `Deposited $${amount.toFixed(2)}`;
success = true;
} else if (tx.type === "withdrawal") {
let amount = withdraw(account, tx.amount);
note = `Withdrew $${amount.toFixed(2)}`;
success = true;
} else {
throw new InvalidTransactionError("Unknown transaction type: " + tx.type);
}
} catch (error) {
note = error.name + ": " + error.message;
} finally {
// finally always runs — we record the transaction regardless
account.log.push({
txNum: txNum,
type: tx.type,
amount: tx.amount,
success: success,
balance: account.balance,
note: note
});
let status = success ? "✅" : "❌";
console.log(`[TX-${txNum}] ${status} ${tx.type.toUpperCase()} $${String(tx.amount).padStart(8)} | Balance: $${account.balance.toFixed(2)} | ${note}`);
}
});
}
// Transactions to process
let transactions = [
{ type: "deposit", amount: 500 },
{ type: "withdrawal", amount: 200 },
{ type: "withdrawal", amount: 1500 }, // insufficient funds
{ type: "deposit", amount: -100 }, // negative deposit
{ type: "deposit", amount: "two hundred" }, // wrong type
{ type: "transfer", amount: 100 }, // unknown type
{ type: "withdrawal", amount: 300 },
{ type: "deposit", amount: 750 }
];
processTransactions(account, transactions);
Expected Stage 2 Output:
--- Processing Transactions ---
[TX-01] ✅ DEPOSIT $ 500 | Balance: $1500.00 | Deposited $500.00
[TX-02] ✅ WITHDRAWAL $ 200 | Balance: $1300.00 | Withdrew $200.00
[TX-03] ❌ WITHDRAWAL $ 1500 | Balance: $1300.00 | InsufficientFundsError: Cannot withdraw 1500. Available balance: 1300
[TX-04] ❌ DEPOSIT $ -100 | Balance: $1300.00 | InvalidTransactionError: Deposit amount must be positive. Got: -100
[TX-05] ❌ DEPOSIT $ two hundred | Balance: $1300.00 | InvalidTransactionError: Deposit amount must be a valid number. Got: two hundred
[TX-06] ❌ TRANSFER $ 100 | Balance: $1300.00 | InvalidTransactionError: Unknown transaction type: transfer
[TX-07] ✅ WITHDRAWAL $ 300 | Balance: $1000.00 | Withdrew $300.00
[TX-08] ✅ DEPOSIT $ 750 | Balance: $1750.00 | Deposited $750.00
Stage 3 — Displaying the Full Account Statement
function printStatement(account) {
let divider = "=".repeat(65);
let thinLine = "-".repeat(65);
console.log("\n" + divider);
console.log(" ACCOUNT STATEMENT");
console.log(" Account: " + account.id);
console.log(" Owner: " + account.owner);
console.log(divider);
console.log(" TX | TYPE | AMOUNT | STATUS | BALANCE");
console.log(thinLine);
account.log.forEach(function(entry) {
let type = entry.type.padEnd(10);
let amount = ("$" + String(entry.amount)).padEnd(10);
let status = entry.success ? "✅ OK " : "❌ FAIL";
let balance = "$" + entry.balance.toFixed(2);
console.log(` ${entry.txNum} | ${type} | ${amount} | ${status} | ${balance}`);
});
console.log(divider);
let successful = account.log.filter(e => e.success);
let failed = account.log.filter(e => !e.success);
let totalIn = successful
.filter(e => e.type === "deposit")
.reduce((sum, e) => sum + e.amount, 0);
let totalOut = successful
.filter(e => e.type === "withdrawal")
.reduce((sum, e) => sum + e.amount, 0);
console.log(" Total Transactions: " + account.log.length);
console.log(" Successful: " + successful.length);
console.log(" Failed: " + failed.length);
console.log(" Total Deposited: $" + totalIn.toFixed(2));
console.log(" Total Withdrawn: $" + totalOut.toFixed(2));
console.log(" Closing Balance: $" + account.balance.toFixed(2));
console.log(divider);
}
printStatement(account);
Expected Stage 3 Output:
=================================================================
ACCOUNT STATEMENT
Account: ACC-2024-001
Owner: Amara Osei
=================================================================
TX | TYPE | AMOUNT | STATUS | BALANCE
-----------------------------------------------------------------
01 | deposit | $500 | ✅ OK | $1500.00
02 | withdrawal | $200 | ✅ OK | $1300.00
03 | withdrawal | $1500 | ❌ FAIL | $1300.00
04 | deposit | $-100 | ❌ FAIL | $1300.00
05 | deposit | $two hundred | ❌ FAIL | $1300.00
06 | transfer | $100 | ❌ FAIL | $1300.00
07 | withdrawal | $300 | ✅ OK | $1000.00
08 | deposit | $750 | ✅ OK | $1750.00
=================================================================
Total Transactions: 8
Successful: 4
Failed: 4
Total Deposited: $1250.00
Total Withdrawn: $500.00
Closing Balance: $1750.00
=================================================================
Reflection Questions
-
finallyran on every transaction — successful or failed. Why is this important for the transaction log? What would be missing if we recorded the log entry only in thetryblock? -
We used two custom error types:
InsufficientFundsErrorandInvalidTransactionError. What is the benefit of having separate types instead of justthrow new Error("...")? -
A programmer who calls
deposit(account, "two hundred")made a coding mistake. A user who tries to withdraw $1500 from an account with $1300 made a business logic error. Should these be handled the same way? How does having different error types help you treat them differently? -
We used
"use strict"at the top. Name one specific silent error that could have occurred in this project without strict mode, and how it would have affected the results. -
In a real bank system, failed transactions would need to be logged to a database for compliance and auditing — not just printed to the console. How would you modify
processTransactionsto store a separatefailedLogarray for compliance review?
Optional Advanced Features
- Add a daily withdrawal limit of $1000. If a withdrawal would exceed the day’s total, throw a custom
DailyLimitError. - Add transaction rollback: if a batch of transactions is meant to succeed or fail together (like a transfer between accounts), catch a failure and undo any earlier successful transactions in the batch.
- Add error frequency reporting: at the end of
printStatement, show how many of each error type occurred (e.g.,InsufficientFundsError: 1,InvalidTransactionError: 3).
Completion Checklist
| Item | Status |
|---|---|
| Silent errors explained with real examples | ✅ |
| Each category of silent error demonstrated | ✅ |
| Strict mode coverage — what it prevents and how to enable | ✅ |
try, catch, finally individually explained |
✅ |
throw statement explained with custom messages |
✅ |
| Re-throwing errors explained | ✅ |
Error object properties: name, message, stack |
✅ |
| Stack trace reading explained step-by-step | ✅ |
| All six built-in error types with examples | ✅ |
Custom error classes with extends Error |
✅ |
instanceof for error type detection |
✅ |
| Error handling patterns and best practices | ✅ |
| Common beginner mistakes documented | ✅ |
| Three applied exercises with real-world scenarios | ✅ |
| Full mini-project (bank processor) across three stages | ✅ |
| Reflection questions answered | ✅ |
Summary: JavaScript errors split into two families — silent failures (swallowed by default, exposed by "use strict") and thrown exceptions (caught with try/catch/finally). Every thrown error is an object with name, message, and stack. The six built-in types (RangeError, ReferenceError, SyntaxError, TypeError, URIError, EvalError) cover technical failures; custom classes that extend Error cover business-logic failures. Robust programs always use strict mode, validate early with throw, handle errors selectively with instanceof, never swallow errors silently, and use finally to guarantee cleanup regardless of outcome.