JavaScript ยท Lesson 22

JavaScript Errors ยท Introduction, Silent Errors, Handling & the Error Object

12 phases  ยท  Build: Project Overview

๐Ÿ‘‹ Welcome to Lesson 22

Work through each phase in order. Complete the task before unlocking the next. Your Build It project unlocks when all phases are done.

๐Ÿ“š 12 phases๐Ÿ—๏ธ Project Overview๐ŸŒ GitHub Pages
Phase 1 of 12
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.


โœ๏ธ Your Task
Practise what you just learned about 1.1 What Is an Error?. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 2 of 12
1.2 The Two Worlds of Errors: Loud and Silent

JavaScript errors divide into two fundamental categories:

CategoryWhat HappensJavaScript's ReactionExample
Loud ErrorsSomething clearly failsThrows an error; stops execution at that lineCalling a method on undefined
Silent ErrorsSomething goes wrong but JavaScript keeps runningNo error is thrown; wrong result producedAssigning 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.


โœ๏ธ Your Task
Practise what you just learned about 1.2 The Two Worlds of Errors: Loud and Silent. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 3 of 12
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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:

javascript
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.


โœ๏ธ Your Task
Practise what you just learned about 1.3 Silent Errors ยท When JavaScript Says Nothing. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 4 of 12
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:

javascript
"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:

javascript
"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 ErrorWithout StrictWith Strict
Undeclared variableCreates globalReferenceError
Write to read-only propertySilently ignoredTypeError
Write to non-writable propertySilently ignoredTypeError
Duplicate parameter namesSecond wins silentlySyntaxError
Delete non-configurable propertySilently ignoredTypeError
Using with statementAllowedSyntaxError
this in function (non-method)Refers to windowundefined

๐Ÿ’ก 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.


โœ๏ธ Your Task
Practise what you just learned about 1.4 Strict Mode ยท Making Silent Errors Loud. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 5 of 12
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.

code
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.

code
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.

code
finally {
  // Always runs โ€” error or no error
}

Full Structure

javascript
try {
  // Attempt something risky
} catch (error) {
  // Handle the error if it happens
} finally {
  // Always runs regardless
}

Micro-Demo 1: Basic try/catch

javascript
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

javascript
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:

code
Loading started...
Failed to parse data: Unexpected token i in JSON at position 2
Loading finished.

๐Ÿ’ก finally is critical for cleaning up resources. Even if the catch block throws another error, finally still runs.


Micro-Demo 3: catch is optional, finally is optional (but not both)

javascript
// 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.

code
throw expression;

You can throw any value, but the best practice is to throw an Error object (covered in 1.6):

javascript
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

javascript
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:

code
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:

javascript
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:

code
Handled TypeError: Input must be a number
0
10

โš ๏ธ Common Beginner Mistake ยท Swallowing errors silently

javascript
// โŒ 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.


โœ๏ธ Your Task
`javascript throw new Error("Something went wrong"); throw new TypeError("Expected a number"); throw new RangeError("Value must be between 0 and 100"); `
Phase 6 of 12
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

PropertyTypeWhat it contains
nameStringThe type of error: "TypeError", "RangeError", etc.
messageStringA human-readable description of what went wrong
stackStringA trace of which functions were called before the error

Micro-Demo: Inspecting an Error object

javascript
try {
  null.toString();
} catch (error) {
  console.log("name:    " + error.name);
  console.log("message: " + error.message);
  console.log("stack:\n" + error.stack);
}

Expected Output:

code
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:

javascript
function c() { null.toString(); }
function b() { c(); }
function a() { b(); }

try {
  a();
} catch (error) {
  console.log(error.stack);
}

Expected Output (simplified):

code
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:

javascript
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):

javascript
let typeErr  = new TypeError("Expected a string");
let rangeErr = new RangeError("Value out of bounds");

โœ๏ธ Your Task
Practise what you just learned about 1.6 The Error Object. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 7 of 12
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.

javascript
// 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.

javascript
try {
  console.log(studentName);   // never declared
} catch (e) {
  console.log(e.name + ": " + e.message);
  // ReferenceError: studentName is not defined
}
javascript
"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.

javascript
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. But JSON.parse() throws SyntaxError at runtime, which is very commonly caught in real programs.


4. TypeError

Thrown when an operation is performed on a value of the wrong type.

javascript
// 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 on null or undefined is 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.

javascript
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.

javascript
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?

SituationError Type
Value outside allowed rangeRangeError
Variable not declared / not in scopeReferenceError
Invalid code syntax / bad JSONSyntaxError
Wrong type used / null property accessTypeError
Malformed URI stringURIError
Generic / custom errorsError

โœ๏ธ Your Task
`javascript let evalErr = new EvalError("eval is disabled here"); console.log(evalErr.name); // "EvalError" console.log(evalErr.message); // "eval is disabled here" ` ยท
Phase 8 of 12
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 a RangeError ยท 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

javascript
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

javascript
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:

code
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:

javascript
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
  }
}

โœ๏ธ Your Task
Practise what you just learned about 1.8 Creating Custom Errors. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 9 of 12
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:

javascript
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:

code
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:

javascript
// 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

javascript
// โŒ 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

javascript
// โŒ 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

MistakeProblemFix
Empty catch blockError disappears silentlyAlways log or handle
Catching too broadlyMasks real bugsUse instanceof to be specific
Not rethrowingUnknown errors get hiddenRethrow what you can't handle
Ignoring silent errorsWrong data silently propagatesUse "use strict" always
Not using finallyResources left open on errorUse finally for cleanup
Throwing plain stringsNo stack trace, no type infoAlways throw new Error(...)

# Section 2 ยท Applied Exercises


โœ๏ธ Your Task
Practise what you just learned about 1.9 Error Handling Patterns and Best Practices. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 10 of 12
Exercise 1 ยท Form Input Validator
๐ŸŽฏ Your Challenge

Warm-Up Mini-Example

โœ๏ธ Task
> Modify validateForm to collect all errors instead of stopping at the first one. Return an array of error messages. (Hint: remove the try/catch from inside the function and wrap each validator call in its own try/catch.) ยท
javascript
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 only
  • email ยท string, must contain @ and .
  • age ยท number or numeric string, must be between 16 and 100
  • password ยท 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.

javascript
"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:

code
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

  1. Why does validateForm stop at the first error instead of collecting all errors? How would you modify it to collect all validation errors before reporting?
  2. validateAge can receive a string and convert it ยท why is this useful in a real form?
  3. What would happen if we caught Error instead of ValidationError and RangeError separately? Would we lose any information?
  4. Why do we throw error at the end of the else branch in validateForm?

What-If Challenge

Modify validateForm to collect all errors instead of stopping at the first one. Return an array of error messages. (Hint: remove the try/catch from inside the function and wrap each validator call in its own try/catch.)


Phase 11 of 12
Exercise 2 ยท Safe JSON Parser
๐ŸŽฏ Your Challenge

Warm-Up Mini-Example

โœ๏ธ Task
Practise what you just learned about Exercise 2 ยท Safe JSON Parser. Open your editor, type the examples above by hand, modify them, and observe what changes.
javascript
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.

javascript
"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:

code
โœ… 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

  1. Why is it important to keep the forEach loop running even after a catch? What would happen if we used a regular for loop and did not catch the error?
  2. The SyntaxError from JSON.parse() and the TypeError we throw manually are both caught by the same catch block. How could you handle them differently?
  3. SystemD has age: "twenty" ยท what is Number("twenty")? Why does isNaN catch this?

Phase 12 of 12
Exercise 3 ยท Stack Trace Reader
๐ŸŽฏ Your Challenge

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.

โœ๏ธ Task
Practise what you just learned about Exercise 3 ยท Stack Trace Reader. Open your editor, type the examples above by hand, modify them, and observe what changes.
javascript
"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:

code
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

  1. Reading the stack trace for Test 2 from bottom to top ยท what was the order of function calls?
  2. The error was thrown in sumScores but caught in the try block that called calculateFinalGrade. How did the error "travel" up through the call stack?
  3. Why is knowing which function threw the error (from the stack trace) more useful than just knowing the error message?

# Section 3 ยท Project Simulation

๐Ÿ—๏ธ Build It โ€” Mini Project
Project Overview

Project Overview

You will build a Bank Transaction Processor ยท a JavaScript program that:

  1. Manages a simple bank account with a balance
  2. Processes a batch of transactions (deposits and withdrawals)
  3. Uses custom errors, strict mode, try/catch/finally, and meaningful error reporting
  4. 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:

starter.html
// 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
}

Lesson 22 complete! ๐ŸŽ‰

You covered: