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.
const students = [{ name: 'Chidinma' }, { name: 'Musa' }];
function firstCity(list) {
return list[0].contact.city;
}
console.log(firstCity(students));That message has three pieces of information and beginners routinely use none of them.
- The type:
TypeErrormeans you did something to a value its type does not support, as opposed toReferenceError, 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].contactisundefined. - 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.
| Error | Almost always means |
|---|---|
ReferenceError: x is not defined | Typo, or declared inside a block |
TypeError: Cannot read properties of undefined | Something before the dot does not exist yet |
TypeError: x is not a function | Typo in the method name, or x is not what you think it is |
SyntaxError: Unexpected token | A 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.
- DevTools, Sources tab, find your file, click a line number. Reload or trigger the code.
- Execution stops before that line runs. Hover any variable to see its value. The Scope panel lists everything in reach.
F10steps over the next line,F11steps into a function,F8runs on to the next breakpoint.- 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.
// 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 see | What it means |
|---|---|
| No request at all | Your code never ran. Check the listener and for an earlier error |
| Status 404 | The URL is wrong. Compare it character by character |
| Status 401 or 403 | Key or token missing, wrong, or not permitted |
| Status 500 | Their server broke. Not your bug, but handle it |
| Status 200 but nothing renders | The data arrived in a different shape. Open the Response tab and look |
(failed) net::ERR_FAILED + a CORS console message | CORS. Day 8. Not fixable from the frontend |
| Pending forever | No 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.
// 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');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
| Category | Ask |
|---|---|
| The normal case | Does it work for ordinary input |
| Boundaries | Exactly on the limit, and one either side |
| Empty | Empty string, empty array, zero, nothing typed |
| Wrong type | Text where a number was expected |
| Too much | A very long string, two thousand rows |
| Twice | Click 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
// 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');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", pluswidthandheightso 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.
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.- Save it, open it, and do not read the source. On a real job there is no answer key and often no readable code.
- Hunt it: click everything, click twice, leave fields empty, use boundary values, keep the Console open the whole time.
- 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.
- 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.