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 19 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. That is a learnable skill with actual technique, and almost nobody is taught it: most beginners debug by changing lines at random until the error goes away, which works occasionally and teaches nothing.

1. Read the error. All of it.

JSapp.js
const students = [{ name: 'Chidinma' }, { name: 'Musa' }];

function firstCity(list) {
  return list[0].contact.city;
}

console.log(firstCity(students));
Console

That message has three pieces of information and beginners routinely use none of them.

  • The type: TypeError means you did something to a value its type does not support, as opposed to ReferenceError, which means the name does not exist at all.
  • The message: "Cannot read properties of undefined (reading 'city')" says exactly which property you were reaching for, so the thing before the dot is what is undefined. list[0].contact is undefined.
  • The stack: the top line is where it broke; the lines below are who called it. Read down until you reach a file you wrote.
ErrorAlmost always means
ReferenceError: x is not definedTypo, or declared inside a block
TypeError: Cannot read properties of undefinedSomething before the dot does not exist yet
TypeError: x is not a functionTypo 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

2. Breakpoints beat console.log

console.log shows you one value at one moment. A breakpoint stops the program and lets you look at everything at that moment, then step forward one line at a time.

  1. DevTools, Sources tab, find your file, click a line number. Reload or trigger the code.
  2. Execution stops before that line runs. Hover any variable to see its value. The Scope panel lists everything in reach.
  3. F10 steps over the next line, F11 steps into a function, F8 runs on to the next breakpoint.
  4. Right-click a breakpoint for a conditional one: id === 47. That is how you debug the one bad row out of two hundred without stopping on the other hundred and ninety-nine.
JSapp.js
// the emergency version: stops the debugger right here
function deleteTask(id) {
  debugger;                        // remove before committing
  state.tasks = state.tasks.filter((t) => t.id !== id);
}

Two console methods worth knowing

console.table(arrayOfObjects) prints a real table and is far easier to scan than a wall of logged objects. console.count('render') tells you how many times something ran, which is how you discover your render is firing forty times per keystroke.

3. The Network tab

When data does not appear, the question is always the same: did the request go out, and what came back? The Console cannot tell you. The Network tab can.

What you seeWhat it means
No request at allYour code never ran. Check the listener and for an earlier error
Status 404The URL is wrong. Compare it character by character
Status 401 or 403Key or token 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) net::ERR_FAILED + a CORS console messageCORS. Day 8. Not fixable from the frontend
Pending foreverNo timeout, and the server never answered

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. Ninety per cent of "the API is not working" turns out to be a typo in the URL or a shape you assumed rather than checked.

4. Testing, at your level

You are not writing a test suite this month. You are learning to think in test cases, which is the part that transfers.

JSapp.js
// a whole testing framework, in five lines
function check(description, actual, expected) {
  const pass = JSON.stringify(actual) === JSON.stringify(expected);
  console.log(`${pass ? 'PASS' : 'FAIL'}  ${description}`);
  if (!pass) console.log(`      expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
}

function gradeFor(score) {
  if (score >= 70) return 'A';
  if (score >= 60) return 'B';
  if (score >= 50) return 'C';
  return 'F';
}

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');
Console

Six tests, and four of them are boundaries. That is deliberate: bugs cluster at the edges. A function that works for 95 and 30 will usually work for everything in between; the ones that break are 49, 50, 69 and 70.

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

Where this goes next

These same categories are what a QA engineer calls edge cases, and the same six lines above are what a framework like Vitest or Jest does with better output and a runner. Learn to think in cases now and the framework is a weekend.

5. Performance: the three that matter

Do not optimise things nobody notices. These three are the ones a beginner actually hits.

One: rebuilding the DOM inside a loop

JSapp.js
// stand-in for the DOM so the cost is visible in Node
let work = 0;
const fakeDom = { _html: '', set innerHTML(v) { work += v.length; this._html = v; },
                  get innerHTML() { return this._html; } };

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

work = 0;
for (const row of rows) { fakeDom.innerHTML += row; }      // re-parses every time
const inLoop = work;

work = 0;
fakeDom.innerHTML = rows.join('');                          // once
const once = work;

console.log('character-work inside the loop:', inLoop.toLocaleString());
console.log('character-work assigning once: ', once.toLocaleString());
console.log('ratio:', Math.round(inLoop / once) + 'x');
Console

That ratio is why "build one string, assign once" was a rule on Day 4 rather than a suggestion. Each += re-serialises everything already there, so the work grows with the square of the number of rows.

Two: work on every keystroke or every scroll

You already fixed this on Day 9. Debounce the search, throttle the scroll, and use IntersectionObserver rather than measuring positions.

Three: images

  • A 4MB photo displayed at 400px wide is 4MB downloaded on somebody's data bundle. Resize before uploading.
  • loading="lazy", plus width and height so the page does not jump.
  • Modern formats where you can. WebP is typically much smaller than the same JPEG.

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, measure again. Optimising a function that runs twice while ignoring one that runs ten thousand times is the most common way people waste an afternoon.

6. Fix this application

Today's main exercise

Ask an AI for a broken app, then debug it as a black box. This is the closest thing to real junior work you can generate for yourself.

TEXTprompt
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.
  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 it: click everything, click twice, leave fields empty, use boundary values, keep the Console 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.

Checkpoint

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

Whatever is immediately before .city. That is the thing that is undefined. Not city itself: the property you asked for is only named to tell 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 at all, 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. An off-by-one in a comparison shows up exactly on the edge and nowhere else.

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

Each assignment re-serialises and re-parses everything already in the element, so the work grows with the square of the row count. Build one string, assign once.

Tick before you move on

  • I used a real breakpoint and stepped through with F10
  • I set a conditional breakpoint at least once
  • I found all my API problems in the Network tab, not by guessing
  • I wrote boundary 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 a moment; logs show one thing · 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

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.