Skip to main content
Published / updated

Debugging Web Applications

Before you start

You need: a frontend calling an API, and code debugging (Article 02).

Time: about 50 minutes, at the keyboard.

Learning objective

Diagnose a browser-side failure and determine, from evidence, whether the fault is in the frontend or the API.

Topics

  • The first question
  • The Network panel
  • Status codes as a routing rule
  • The Console
  • Elements and why CSS "doesn't work"
  • Application: storage, tokens and cookies
  • CORS
  • Sources and breakpoints in JavaScript

The first question

Something on the page is wrong. Before reading any code, open DevTools (F12) and answer one question:

Did the request happen, and what came back?

Network tab → find the request → status code → Response tab
ObservationConclusion
No request at allFrontend — the event handler never fired, or a condition prevented the call
Request sent, 200, correct dataFrontend — the rendering is wrong
Request sent, 200, wrong dataBackend — the query or mapping is wrong
Request sent, 4xxUsually frontend — bad input, missing token
Request sent, 5xxBackend — an unhandled exception
Request sent, CORS errorBackend configuration
Request pending foreverBackend hung, or the wrong URL

This single check ends most frontend-versus-backend arguments in thirty seconds, and it should come before opening an editor.

The Network panel

Turn on Preserve log before reproducing, or a redirect wipes the evidence.

For each request:

Sub-tabShows
HeadersURL, method, status, request and response headers
PayloadWhat was sent
Preview / ResponseWhat came back
TimingWhere the time went

Check in this order:

  1. The URL. Is it what you expect? A double slash, a missing /api, undefined in the path — all common, all visible here.
  2. The method. A GET where the API expects POST returns 405.
  3. Request headers. Is Authorization: Bearer … present? Is Content-Type: application/json set?
  4. The payload. Are the field names exactly what the API expects? Casing matters.
  5. The response. Read the body — an API usually explains the 400.
Filter: Fetch/XHR hides scripts, images and stylesheets

Copy as cURL (right-click → Copy → Copy as cURL) reproduces the exact request outside the browser. Run it in a terminal or import it into Postman: if it fails there too, the frontend is exonerated.

Timing separates a slow API from a slow page. Waiting (TTFB) is the server thinking; Content Download is the payload size. 2 seconds of TTFB is a backend problem; 2 seconds of download is a payload problem.

Status codes

CodeMeaningOwner
200OK
201Created
204No content
304Not modified, cachedUsually fine
400Bad requestFrontend — read the response body
401UnauthenticatedToken missing, malformed or expired
403Authenticated, not allowedBackend authorisation — correct behaviour, usually
404Not foundWrong URL, or the record does not exist
405Method not allowedWrong verb
415Unsupported media typeContent-Type missing or wrong
500Server errorBackend — an unhandled exception
502 / 504Gateway error / timeoutInfrastructure, or the API is down

401 versus 403 is the distinction people get wrong. 401 means "I do not know who you are"; 403 means "I know, and you may not". A 403 on a page a user should reach is an authorisation bug; a 403 on one they should not is the system working.

415 almost always means Content-Type: application/json was not sent. A body posted as plain text produces it, and the message never says so directly.

Never treat a 500 as a frontend problem. It is an unhandled exception on the server; the fix is in the API logs.

The Console

console.log(student);
console.table(students); // an array of objects as a grid
console.error(err);
console.warn("Falling back to cached list");
console.dir(element); // the DOM object, not the rendered element
console.count("render"); // how many times this ran
console.time("load"); console.timeEnd("load");

console.table on an array of objects is far more readable than console.log, and it makes a wrong field name obvious at a glance.

Shortcuts in the console itself:

$0 // the element currently selected in Elements
$('.student-row') // document.querySelector
$$('.student-row') // document.querySelectorAll, returns an array
copy(students) // copy to clipboard

Read the stack trace in a console error. Click the file:line link to jump straight to the source. Frames from framework bundles above your own code are noise.

Errors you will meet:

ErrorCause
Cannot read properties of undefined (reading 'name')The object is not there yet — data still loading, or the field is named differently
X is not a functionWrong import, or the value is not what you think
Unexpected token < in JSON at position 0The response was HTML, not JSON — usually a 404 or an error page
Failed to fetchNetwork, wrong URL, or CORS

Unexpected token < is worth memorising. It means the code called .json() on an HTML error page — so the real problem is the status code, visible in Network.

Elements

Elements shows the live DOM, not the source. View Source shows what the server sent; Elements shows what the DOM is now, after JavaScript has run. When they differ, the difference is what your JavaScript did.

When "the CSS isn't working", there are three causes and the Styles pane distinguishes them:

Evidence in StylesCause
The rule is listed but struck throughOverridden — a more specific selector wins
The rule is not listed at allSelector does not match, or the file did not load
The rule applies but nothing movesThe property is not doing what you think — check Computed and the box model

Check Computed for the final value, and the box model diagram for what is actually margin, border and padding.

:hov forces :hover, :focus and :active so you can style a state without chasing it with the mouse.

Break on a DOM change: right-click an element → Break on → attribute modifications / subtree modifications. This is how you find the code that adds a class or empties a container, when you have no idea where it lives.

Application

SectionCheck
Local StorageIs the token there? Is it the current one?
Session StorageCleared on tab close — deliberate?
CookiesHttpOnly, Secure, SameSite, expiry
Service WorkersStale cache — unregister while debugging
Clear storageReproduce the first-time-user experience

Most "randomly logged out" reports are token expiry. Copy the token, decode it at jwt.io, and read exp. If it is in the past, the bug is that the app does not refresh or redirect — not that the login broke.

A service worker serving a stale bundle explains "my change didn't deploy" when the change is definitely deployed. Unregister it, hard reload, and check again before investigating the pipeline.

Ctrl+Shift+R hard reloads. With DevTools open, Network → Disable cache is more reliable.

CORS

Access to fetch at 'https://localhost:7099/api/students' from origin
'http://localhost:5173' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested resource.

CORS is enforced by the browser and configured on the server. Nothing you write in the frontend fixes it.

builder.Services.AddCors(options =>
{
options.AddPolicy("SchoolPortal", policy =>
{
policy.WithOrigins("http://localhost:5173")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});

app.UseCors("SchoolPortal"); // after UseRouting, before UseAuthentication

Three things that catch people out:

AllowAnyOrigin and AllowCredentials cannot be combined. The browser rejects it, and the error does not explain why.

UseCors in the wrong position does nothing. It must come after UseRouting and before UseAuthentication.

Postman does not enforce CORS. A request working in Postman and failing in the browser is the signature of a CORS problem — not evidence that the API is fine.

A OPTIONS request in the Network tab is the preflight. If it fails, the real request never goes out.

Sources and JavaScript breakpoints

async function loadStudents(schoolId) {
const response = await fetch(`/api/students?schoolId=${schoolId}`); // breakpoint
const students = await response.json();
renderStudents(students);
}
  • Click the line number to set a breakpoint
  • Right-click → Add conditional breakpoint: schoolId === 3
  • Right-click → Add logpoint: logs without pausing and without editing the file
  • debugger; in code stops when DevTools is open

F10 step over, F11 step into, Shift+F11 step out — the same keys as Visual Studio.

Enable "Pause on caught exceptions" in the Sources panel when a value is wrong and nothing is logged. It is the browser equivalent of first-chance exceptions.

Check for source maps if you are stepping through minified code. Without them, you are debugging one long line.

Diagnosing

SymptomFirst check
Button does nothingNetwork — was a request sent at all?
Blank pageConsole — a JavaScript error stopped rendering
Unexpected token <Network — the response was HTML
Data appears then vanishesA second request overwriting, or a re-render
401 on every callApplication → token present and unexpired?
Works in Postman, not the browserCORS
CSS ignoredElements → Styles — struck through, or absent?
Change not appearingService worker or cache; hard reload
Slow pageNetwork → Timing — TTFB or download?

Errors you will hit

ObservationConclusion
No request in the Network tabFrontend — the handler never fired
200 with correct data, wrong displayFrontend rendering
200 with wrong dataBackend
4xxUsually the caller — read the response body
5xxBackend — check the API log
CORS errorServer configuration

One check settles most frontend-versus-backend arguments: open Network, find the request, read the status and the body.

Common mistakes

  • Reading code before opening the Network tab
  • Not enabling Preserve log, losing the request to a redirect
  • Treating a 500 as a frontend bug
  • Trying to fix CORS in JavaScript
  • Concluding the API is fine because Postman works
  • Not reading the response body of a 400
  • Assuming Elements shows the source
  • Ignoring a stale service worker
  • Debugging minified code without source maps
  • Missing Content-Type: application/json and puzzling over 415

Practice

The course exercise is debug a browser failure.

  1. Break a button handler so no request fires. Confirm from Network that nothing was sent.
  2. Call a wrong URL and identify the 404 from Network rather than from code.
  3. Send a POST without Content-Type. Get the 415 and fix it.
  4. Send a request with a field name in the wrong case. Read the 400 response body.
  5. Remove the Authorization header and get a 401. Then use a token for a role without permission and get a 403. Note which is which.
  6. Copy a failing request as cURL and reproduce it outside the browser.
  7. Return HTML from an endpoint and trigger Unexpected token <. Trace it back to the status code.
  8. Use console.table on a list of students and spot a wrong field name.
  9. Select an element, then manipulate it with $0 in the console.
  10. Break CSS three ways — specificity, a wrong selector, and a file that fails to load — and identify each from the Styles pane.
  11. Set a DOM breakpoint on subtree modification and find the code that clears a list.
  12. Decode an expired token from local storage at jwt.io and confirm the 401 matches exp.
  13. Call your API from a different origin without a CORS policy, then fix it server-side.
  14. Combine AllowAnyOrigin with AllowCredentials and see it fail.
  15. Move UseCors after UseAuthentication and confirm it stops working.
  16. Set a conditional breakpoint and a logpoint in Sources on a function called many times.

Exercises 13 to 15 are the CORS set. Do all three — the failure modes look identical from the frontend and have different fixes.

You can now

  • Decide from the Network tab which side owns a failure
  • Read status codes correctly, including 401 versus 403
  • Diagnose CSS from the Styles pane
  • Inspect storage and decode an expired token
  • Recognise a CORS error and say where it is fixed

Review questions

  1. What does "no request in the Network tab" tell you?
  2. What is the difference between 401 and 403?
  3. Why does a request working in Postman not prove the API is correct for the browser?
  4. What three causes make CSS appear not to work, and how does Styles distinguish them?

Next: Debugging APIs