Courses Learn Blog Contact
Get Started →
Lesson 14

Day 14: Debugging, Testing and Performance

Reading stack traces, breakpoints, the Network tab, boundary test cases, the three performance mistakes beginners make, and a broken app to fix as a black box.

Webbo3 Level 2: JavaScript Development 16 min read Free

Webbo3 Level 2 · JavaScript Development · Day 14 of 20

Debugging, Testing and Performance

A developer day. You are given broken code and you have to find out why, which is most of what the job actually is.

By the end of today you can

  • Read a stack trace instead of skipping past it
  • Use breakpoints to stop time and look at your own variables
  • Find the request that failed in the Network tab
  • Write test cases for your own code, and know what to test
  • Spot the three performance mistakes beginners actually make

Nobody writes code that works first time. Professionals are not people who avoid bugs; they are people who find them quickly.

Finding bugs is a learnable skill with real technique, and almost nobody is taught it. Most beginners debug by changing lines at random until the error goes away. That works occasionally and teaches nothing. Today you learn the technique.

1. Read the Error. All of It.

Definition

An error message is JavaScript telling you three things: what kind of problem happened (the type), what exactly went wrong (the message), and the chain of function calls that led there (the stack trace).

Real-Life Example

A mechanic's report does not just say "car broken". It says "brake fault, front left pad worn through, noticed after the Ikorodu trip". Kind of fault, exact part, and how you got there. An error message is that report, and beginners throw it away unread.

Types: The Errors You Will Meet

ErrorAlmost always means
ReferenceError: x is not definedA typo, or the name was declared inside a block
TypeError: Cannot read properties of undefinedSomething before the dot does not exist yet
TypeError: x is not a functionA typo in the method name, or x is not what you think it is
SyntaxError: Unexpected tokenA missing bracket or comma, usually just above the reported line
Uncaught (in promise)An async function with no catch

How to Read One

  • The type tells you the family. TypeError means you did something to a value its type does not support. ReferenceError means the name does not exist at all.
  • The message names the property you were reaching for. The thing before the dot is the thing that is undefined.
  • The stack reads top to bottom. The top line is where it broke; each line below it is the function that called the one above. Each line ends with (file:line:column).
What this code doesWe want to see a real error with its whole stack trace, and read each part of it. The code reaches for a city on a student who has no contact details.
app.js
1
2
3
4
5
6
7
8
9
// Two students, and neither has a contact object
const students = [{ name: 'Chidinma' }, { name: 'Musa' }];

// Return the first student's city
function firstCity(list) {
  return list[0].contact.city;
}

console.log(firstCity(students));
OUTPUT   TERMINAL
➜ webbo3 node app.js
TypeError: Cannot read properties of undefined (reading 'city')
    at firstCity (app.js:6:26)
    at Object.<anonymous> (app.js:9:13)

Read it in order. TypeError: we used a value the wrong way. reading 'city': so the thing before .city is undefined, which is list[0].contact. at firstCity (app.js:6:26): it broke on line 6, column 26, and the next line down says line 9 called it. You now know the file, the line and the value without guessing.

The next step is to look at the value itself. Log the thing before the dot:

What this code doesWe want to confirm which value is undefined before changing anything, by logging the object and then the property the error pointed at.
app.js
1
2
3
4
5
const students = [{ name: 'Chidinma' }, { name: 'Musa' }];

// Log the value just before the failing dot
console.log(students[0]);
console.log(students[0].contact);
OUTPUT   TERMINAL
➜ webbo3 node app.js
{ name: 'Chidinma' }
undefined

The student has a name and no contact. The code was right; the assumption about the data was wrong. The fix is to check before reaching:

What this code doesWe want a function that copes with students who have no contact details, instead of crashing on the first one.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const students = [{ name: 'Chidinma' }, { name: 'Musa', contact: { city: 'Kano' } }];

// Return the city, or a clear fallback when there are no contact details
function cityOf(student) {
  if (!student.contact) {
    return 'no city given';
  }
  return student.contact.city;
}

// Every student, whether or not they have contact details
for (const student of students) {
  console.log(student.name + ':', cityOf(student));
}
OUTPUT   TERMINAL
➜ webbo3 node app.js
Chidinma: no city given
Musa: Kano

Two more errors you will see weekly. First, a typo in a name:

What this code doesWe want to see what a misspelt variable produces, so the ReferenceError is familiar when you meet it.
app.js
1
2
3
4
const studentName = 'Zainab';

// A typo: studentname instead of studentName
console.log(studentname);
OUTPUT   TERMINAL
➜ webbo3 node app.js
ReferenceError: studentname is not defined
    at Object.<anonymous> (app.js:4:13)
What this code doesWe want to see "is not a function", which almost always means a misspelt method name.
app.js
1
2
3
4
5
const names = ['Ade', 'Bola'];

// A typo in the method name: push is spelt psuh
names.psuh('Chidinma');
console.log(names);
OUTPUT   TERMINAL
➜ webbo3 node app.js
TypeError: names.psuh is not a function
    at Object.<anonymous> (app.js:4:7)
Try This

Break your Day 9 project on purpose three ways: misspell a variable, misspell a method, and reach for a property that does not exist. Read each error aloud: type, message, file and line. Then fix each one using only what the error told you.

Picture This

Picture the mechanic's report: kind of fault, exact part, and the trip that caused it. Read all three lines before you pick up a spanner.

2. Breakpoints Beat console.log

Definition

A breakpoint is a line where you tell the browser to pause your program before running it. While it is paused you can see every variable in reach, then move forward one line at a time.

Real-Life Example

A football replay. console.log is a single photo of the match: one moment, one angle. A breakpoint is pausing the replay: you see every player where they stand, then step forward frame by frame to watch the foul happen.

Types: How to Pause

KindHowUse it when
Line breakpointDevTools, Sources tab, click a line numberYou know roughly where it goes wrong
Conditional breakpointRight-click a line number, add a condition such as id === 47One bad row out of two hundred
debugger statementWrite it in your codeYou cannot find the file in Sources

Moving While Paused

KeyDoes
F10Step over: run the next line and pause again
F11Step into: go inside the function on this line
F8Resume: run on to the next breakpoint

While paused, hover any variable to see its value, and read the Scope panel for everything in reach.

The debugger statement pauses only when DevTools is open. With DevTools closed the browser ignores it, which is why a forgotten one does not break the site but must still be removed before you commit.

What this code doesWe want to prove that a debugger statement does nothing while DevTools is closed: the delete still runs and the list still updates.
index.html
1
2
<ul id="tasks"></ul>
<button id="delete">Delete task 2</button>
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let tasks = [{ id: 1, text: 'Read Day 14' }, { id: 2, text: 'Fix the bug' }];

// Draw the list from the tasks array
function render() {
  document.querySelector('#tasks').innerHTML =
    tasks.map((task) => `<li>${task.text}</li>`).join('');
}

// Remove one task by id
function deleteTask(id) {
  debugger; // pauses here only when DevTools is open; remove before committing
  tasks = tasks.filter((task) => task.id !== id);
  render();
  console.log('tasks left:', tasks.length);
}

document.querySelector('#delete').addEventListener('click', () => deleteTask(2));
render();
BROWSERindex.html · after clicking Delete with DevTools closed
CONSOLE   BROWSER DEVTOOLS
tasks left: 1

Two Console Methods Worth Knowing

console.table(list) prints an array of objects as a real table, far easier to scan than a wall of logged objects. console.count(label) prints how many times that line has run with that label.

What this code doesWe want to see three student objects at a glance, as rows and columns instead of nested braces.
app.js
1
2
3
4
5
6
7
8
const students = [
  { name: 'Chidinma', score: 82, city: 'Lagos' },
  { name: 'Musa', score: 47, city: 'Kano' },
  { name: 'Zainab', score: 69, city: 'Abuja' },
];

// One row per student, one column per property
console.table(students);
OUTPUT   TERMINAL
➜ webbo3 node app.js
┌─────────┬────────────┬───────┬─────────┐
│ (index) │ name       │ score │ city    │
├─────────┼────────────┼───────┼─────────┤
│ 0       │ 'Chidinma' │ 82    │ 'Lagos' │
│ 1       │ 'Musa'     │ 47    │ 'Kano'  │
│ 2       │ 'Zainab'   │ 69    │ 'Abuja' │
└─────────┴────────────┴───────┴─────────┘
What this code doesWe want to find out how often the list redraws while someone types. We type "Lagos" one key at a time and let console.count keep score.
index.html
1
2
<input id="search" placeholder="Search by city">
<p id="result"></p>
app.js
1
2
3
4
5
6
7
8
9
10
// Redraw the result; count every call
function render(text) {
  console.count('render');
  document.querySelector('#result').textContent = `Searching for "${text}"`;
}

// Every keystroke triggers a redraw
document.querySelector('#search').addEventListener('input', (event) => {
  render(event.target.value);
});
BROWSERindex.html · after typing Lagos one key at a time
CONSOLE   BROWSER DEVTOOLS
render: 1
render: 2
render: 3
render: 4
render: 5

Five letters, five redraws. With a list of two thousand rows, that is how an app starts to lag, and console.count is how you catch it. The fix is the debounce from Day 9.

Try This

Put a breakpoint inside your project's render function. Trigger it, then press F10 line by line and watch the Scope panel change. Then right-click the breakpoint, add a condition, and make it pause for only one item.

Picture This

Picture the paused replay: every player frozen where they stand, and you stepping forward one frame at a time until you see the foul.

3. The Network Tab

Definition

The Network tab in DevTools lists every request your page makes, with its status, its headers and exactly what came back. When data does not appear, the question is always: did the request go out, and what came back? The Console cannot answer that. The Network tab can.

Real-Life Example

A dispatch rider's delivery log. When a parcel does not arrive, you do not guess. You check the log: was it picked up, was the address right, what did the person at the door say?

Types: What You See and What It Means

What you seeWhat it means
No request at allYour code never ran. Check the listener and look for an earlier error
Status 404The URL is wrong. Compare it character by character
Status 401 or 403The key or token is missing, wrong, or not permitted
Status 500Their server broke. Not your bug, but handle it
Status 200 but nothing rendersThe data arrived in a different shape. Open the Response tab and look
(failed) plus a CORS messageCORS, from Day 8. Not fixable from the frontend
Pending foreverNo timeout, and the server never answered

Both examples below use the practice API from Day 8. The first has a one-letter typo in the URL; the second gets a 200 and still shows nothing.

What this code doesWe want to see what a mistyped URL looks like from the code's side, by logging the status the server sends back.
index.html
1
<ul id="courses"></ul>
app.js
1
2
3
4
5
6
7
8
9
10
11
12
// A typo: /course instead of /courses
async function loadCourses() {
  const response = await fetch('https://api.webbo3.test/course');
  console.log('status:', response.status);

  if (!response.ok) {
    document.querySelector('#courses').textContent = 'Could not load courses.';
    return;
  }
}

loadCourses();
BROWSERindex.html · after the request came back
CONSOLE   BROWSER DEVTOOLS
Failed to load resource: the server responded with a status of 404 (Not Found)
status: 404
What this code doesWe want to see the most confusing case: status 200, a clean console, and an empty list. The code expects an array; the API wraps it in an object.
index.html
1
2
<ul id="courses"></ul>
<p id="count"></p>
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function loadCourses() {
  const response = await fetch('https://api.webbo3.test/courses');
  const body = await response.json();
  console.log('status:', response.status);

  // The code assumes the list is in body.courses (or nothing, if missing)
  const courses = body.courses || [];
  document.querySelector('#courses').innerHTML =
    courses.map((course) => `<li>${course.title}</li>`).join('');
  document.querySelector('#count').textContent = `${courses.length} courses shown`;

  // What actually came back: look at it, as the Response tab would show
  console.log('response body:', JSON.stringify(body));
}

loadCourses();
BROWSERindex.html · after a 200 that rendered nothing
CONSOLE   BROWSER DEVTOOLS
status: 200
response body: {"data":[{"id":1,"title":"JavaScript"},{"id":2,"title":"Power BI"}]}

The body shows the courses sitting inside data. The fix is one word: read body.data instead of body.courses. You find it in ten seconds by looking, or in an hour by guessing.

Do This Before Asking Anybody for Help

Open Network, tick Preserve log, reproduce the problem, and read the failing row: its status, its Response tab and its Headers tab. Nine times in ten, "the API is not working" is a typo in the URL or a shape you assumed rather than checked.

Picture This

Picture the delivery log: picked up or not, right address or not, and what the person at the door said. Read it before you blame the rider.

4. Testing, at Your Level

Definition

A test is a small piece of code that runs your function with a chosen input and checks the answer against the one you expected. You are not writing a test suite this month; you are learning to think in test cases, which is the part that carries over to every job.

Real-Life Example

A tailor measuring a customer. They do not only check that the trousers fit standing still. They ask you to sit, to bend, to lift your leg: the positions where a seam gives way.

A Whole Testing Tool in Six Lines

To compare two answers, even arrays and objects, we turn both into text with JSON.stringify and compare the text. If they differ, we print both, so the failure tells you what went wrong.

What this code doesWe want to check a grading function with six chosen scores, most of them sitting exactly on a grade boundary.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Run one check and print PASS or FAIL with a description
function check(description, actual, expected) {
  const pass = JSON.stringify(actual) === JSON.stringify(expected);
  if (pass) {
    console.log(`PASS  ${description}`);
  } else {
    console.log(`FAIL  ${description}`);
    console.log(`      expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
  }
}

// The function under test: turn a score into a grade
function gradeFor(score) {
  if (score >= 70) return 'A';
  if (score >= 60) return 'B';
  if (score >= 50) return 'C';
  return 'F';
}

// One ordinary case, four boundaries, one extreme
check('a clear A', gradeFor(95), 'A');
check('exactly on the A boundary', gradeFor(70), 'A');
check('just below the A boundary', gradeFor(69), 'B');
check('exactly on the pass boundary', gradeFor(50), 'C');
check('just below passing', gradeFor(49), 'F');
check('zero', gradeFor(0), 'F');
OUTPUT   TERMINAL
➜ webbo3 node app.js
PASS  a clear A
PASS  exactly on the A boundary
PASS  just below the A boundary
PASS  exactly on the pass boundary
PASS  just below passing
PASS  zero

Six tests, and four are boundaries. That is deliberate: bugs cluster at the edges. Now watch a test catch one. Someone "tidies" the code and writes > where it said >=:

What this code doesWe want to see the boundary test catch a one-character bug that the ordinary tests miss completely.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function check(description, actual, expected) {
  const pass = JSON.stringify(actual) === JSON.stringify(expected);
  if (pass) {
    console.log(`PASS  ${description}`);
  } else {
    console.log(`FAIL  ${description}`);
    console.log(`      expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
  }
}

// The bug: > instead of >= on the pass mark
function gradeFor(score) {
  if (score >= 70) return 'A';
  if (score >= 60) return 'B';
  if (score > 50) return 'C';
  return 'F';
}

check('a clear pass', gradeFor(55), 'C');
check('a clear fail', gradeFor(20), 'F');
check('exactly on the pass boundary', gradeFor(50), 'C');
OUTPUT   TERMINAL
➜ webbo3 node app.js
PASS  a clear pass
PASS  a clear fail
FAIL  exactly on the pass boundary
      expected "C", got "F"

The ordinary cases pass. A student with exactly 50 would have been failed, and only the boundary test noticed.

Types: What to Test

CategoryAsk
The normal caseDoes it work for ordinary input?
BoundariesExactly on the limit, and one either side
EmptyEmpty string, empty array, zero, nothing typed
Wrong typeText where a number was expected
Too muchA very long string, two thousand rows
TwiceClick submit twice quickly. Does it do it twice?
What this code doesWe want to run the "Empty" category against an average function, because nobody thinks about an empty class until one exists.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function check(description, actual, expected) {
  const pass = JSON.stringify(actual) === JSON.stringify(expected);
  if (pass) {
    console.log(`PASS  ${description}`);
  } else {
    console.log(`FAIL  ${description}`);
    console.log(`      expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
  }
}

// Average a list of scores, rounded to a whole number
function average(scores) {
  let total = 0;
  for (const score of scores) {
    total += score;
  }
  return Math.round(total / scores.length);
}

check('normal', average([60, 70, 80]), 70);
check('one score', average([45]), 45);
check('empty list', average([]), 0);
OUTPUT   TERMINAL
➜ webbo3 node app.js
PASS  normal
PASS  one score
FAIL  empty list
      expected 0, got null

The empty list gives null after JSON.stringify, because 0 divided by 0 is NaN (not a number). On screen that would read "Average: NaN". One line fixes it:

What this code doesWe want the same three tests to pass after guarding the empty case.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function check(description, actual, expected) {
  const pass = JSON.stringify(actual) === JSON.stringify(expected);
  if (pass) {
    console.log(`PASS  ${description}`);
  } else {
    console.log(`FAIL  ${description}`);
    console.log(`      expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
  }
}

function average(scores) {
  // Nothing to average: say 0 instead of dividing by zero
  if (scores.length === 0) return 0;

  let total = 0;
  for (const score of scores) {
    total += score;
  }
  return Math.round(total / scores.length);
}

check('normal', average([60, 70, 80]), 70);
check('one score', average([45]), 45);
check('empty list', average([]), 0);
OUTPUT   TERMINAL
➜ webbo3 node app.js
PASS  normal
PASS  one score
PASS  empty list
Where This Goes Next

These categories are what a QA engineer calls edge cases, and the six-line check function is what a framework such as Vitest or Jest does with better output and a runner. Learn to think in cases now and the framework takes a weekend.

Try This

Pick one function from your own project. Write at least six checks for it using the table above: one normal case, the boundaries, empty, and wrong type. Count how many fail the first time.

Picture This

Picture the tailor: standing still proves nothing. Sit, bend, lift your leg, because that is where the seam gives way.

5. Performance: The Three That Matter

Definition

Performance is how quickly your page loads and responds. Do not optimise things nobody notices. These three are the ones a beginner actually hits.

Real-Life Example

A shop with a slow till. Customers do not care how the till works inside; they care that the queue is not moving. Fix the thing that makes the queue slow, not the paint on the counter.

One: Rebuilding the DOM Inside a Loop

Every time you write element.innerHTML += row, the browser takes everything already in the element, turns it back into text, adds the row, and reads the whole lot again. So each row costs more than the one before it.

To make that cost visible in Node, where there is no page, we use a stand-in: a function that adds up how many characters it is handed, which is roughly how much the browser must re-read. Array.from({ length: 500 }, (_, i) => ...) builds an array of 500 items, calling the arrow function with each position i. toLocaleString() prints a big number with commas.

What this code doesWe want to measure how much more work adding rows one at a time costs, compared with building one string and assigning it once.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Stand-in for innerHTML: count the characters the browser would re-read
let work = 0;
let html = '';
function setInnerHTML(value) {
  work += value.length;
  html = value;
}

// 500 rows of list HTML
const rows = Array.from({ length: 500 }, (_, i) => `<li>Row ${i}</li>`);

// Slow: add one row at a time, the way innerHTML += does
work = 0;
html = '';
for (const row of rows) {
  setInnerHTML(html + row);
}
const inLoop = work;

// Fast: build one string, assign it once
work = 0;
setInnerHTML(rows.join(''));
const once = work;

console.log('work inside the loop:', inLoop.toLocaleString());
console.log('work assigning once: ', once.toLocaleString());
console.log('ratio:', Math.round(inLoop / once) + 'x');
OUTPUT   TERMINAL
➜ webbo3 node app.js
work inside the loop: 1,953,995
work assigning once:  7,890
ratio: 248x

That is why "build one string, assign once" was a rule on Day 4. Here is the same thing in a real browser, timed with performance.now(), which returns the time in milliseconds.

What this code doesWe want to time both approaches in Chromium with 3,000 rows, so the difference is measured, not claimed.
index.html
1
2
3
<ul id="slow"></ul>
<ul id="fast"></ul>
<p id="result"></p>
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const rows = Array.from({ length: 3000 }, (_, i) => `<li>Row ${i}</li>`);

// Slow: innerHTML += inside the loop
let start = performance.now();
const slow = document.querySelector('#slow');
for (const row of rows) {
  slow.innerHTML += row;
}
const slowMs = Math.round(performance.now() - start);

// Fast: one string, one assignment
start = performance.now();
document.querySelector('#fast').innerHTML = rows.join('');
const fastMs = Math.round(performance.now() - start);

// Empty both lists again; show only the timings
slow.innerHTML = '';
document.querySelector('#fast').innerHTML = '';
document.querySelector('#result').textContent =
  `innerHTML += in a loop: ${slowMs} ms. Assigned once: ${fastMs} ms.`;
BROWSERindex.html · after rendering 3,000 rows both ways

Two: Work on Every Keystroke or Scroll

You saw console.count reach 5 for one five-letter word in Section 2. You already fixed this on Day 9: debounce the search, throttle the scroll, and use IntersectionObserver rather than measuring positions.

Three: Images

A 4 MB photo shown 400 pixels wide is still 4 MB downloaded on somebody's data bundle.

  • Resize before uploading.
  • loading="lazy" so off-screen images wait until they are needed.
  • width and height so the page keeps a space for the image and does not jump when it arrives.
  • Modern formats where you can. WebP is usually much smaller than the same JPEG.
What this code doesWe want an image tag that does all four image fixes at once.
index.html
1
2
3
4
5
<!-- Resized to 800px wide, in WebP, reserved space, loaded only when near the screen -->
<img src="images/class-photo-800.webp"
     alt="Students at the Webbo3 Lagos class"
     width="800" height="533"
     loading="lazy">
NOTE   
The browser reads width and height to reserve space, and loading="lazy" to delay the download.
Measure Before You Optimise

Every performance change makes code harder to read. Run Lighthouse and the Performance tab first, find the actual slow thing, fix that, and measure again. Speeding up a function that runs twice while ignoring one that runs ten thousand times is the most common way to waste an afternoon.

Picture This

Picture the slow till: fix the thing that holds up the queue, and leave the paint alone.

6. Fix This Application

Today's main exercise. Ask an AI for a broken app, then debug it without reading the code. This is the closest thing to real junior work you can generate for yourself.

What this code doesWe want an app with four hidden bugs, one of each kind taught today, and an answer key we promise not to read yet.
prompt.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
Build me a single-file HTML page (HTML, CSS and JavaScript in one file, no
libraries) for a small student list app. It should let me add a student with a
name and a score, list them, filter by pass or fail, and show an average.

Now plant exactly 4 BUGS of these different kinds:
1. One that throws an error visible in the Console
2. One silent wrong result: it displays a number that is simply incorrect
3. One that only appears at a boundary or after a repeated action
4. One that only appears when a field is left empty

Rules: it must look normal on a casual click-through. Do NOT tell me what the
bugs are. No comments describing them. Put the ANSWER KEY in a separate code
block at the very end.
NOTE   
Paste this into any AI assistant. Save the page it gives you as bugs.html.
  1. Save it, open it, and do not read the source. On a real job there is no answer key and often no readable code.
  2. Hunt: click everything, click twice, leave fields empty, use boundary values, and keep the Console and Network tab open the whole time.
  3. For each bug, write down the symptom, how you found it, the cause and the fix. That is a bug report, and it is what Day 20 will expect.
  4. Only then read the answer key. Two out of four is a normal first attempt; three is good; four means generate a harder one with six.

Do This Now

  1. Break your own project three ways and fix each one from the error message alone.
  2. Set a real breakpoint, step through with F10, then set a conditional breakpoint.
  3. Find one real request in the Network tab and read its status, Response and Headers.
  4. Write six checks for one of your own functions, including the boundaries and the empty case.
  5. Do the Fix This Application exercise and write a bug report for every bug you find.

Checkpoint

Q1. "TypeError: Cannot read properties of undefined (reading 'city')". What do you look at first?

Whatever is immediately before .city, because that is the thing that is undefined. The property named in the message only tells you where you were reaching.

Q2. Data does not appear and the Console is clean. Where do you look?

The Network tab. Did the request go out, what status came back, and what is in the Response tab? A clean console with no data usually means a 200 with a shape you did not expect.

Q3. Why test 49, 50, 69 and 70 rather than 20 and 80?

Because bugs live at boundaries. A > written where >= was meant shows up exactly on the edge and nowhere else.

Q4. Why is innerHTML += inside a loop slow?

Each assignment re-reads everything already in the element, so every row costs more than the one before. Build one string and assign it once.

Tick Before You Move On

  • ☐ I read a full stack trace down to my own file and line
  • ☐ I used a real breakpoint and stepped through with F10
  • ☐ I set a conditional breakpoint at least once
  • ☐ I found my API problems in the Network tab, not by guessing
  • ☐ I wrote boundary and empty test cases for one of my own functions
  • ☐ I found at least two of the four planted bugs without reading the code

Quick Recap

Read the whole error: type, message, then down the stack to your own file. Breakpoints show you everything at one moment; a log shows one thing. The Network tab answers "did it go out, and what came back?" Test boundaries, empties and doubles, because that is where bugs live. Measure before optimising, and fix the thing that actually runs often.

Final Scenario

A tutor messages you at 9pm: "The average on Musa's class page says NaN, and only his."

You do not change a single line yet. You open the page with the Network tab open and see a 200. You open the Response tab and see that his class has no scores yet: an empty array, and 0 divided by 0 is NaN. You write a check for average([]), watch it fail, add the guard, and watch it pass.

Ten minutes, no guessing, and a test that makes sure it never comes back. That is debugging.

Tomorrow, Day 15: your second solo project, and the document you write before you write any code.

Learn this with a real instructor

Free lessons take you a long way on your own. The full programmes add live classes, assignments, Skill Points and a certificate — built for African students.