on this page

Something breaks. The page is blank, or the button does nothing, or the terminal is now a wall of red. Your heart rate ticks up, you grab the scariest-looking line, paste it into a search box, and start trying random fixes from a Stack Overflow answer dated 2017.

We have all done this. It sometimes even works. But that is gambling dressed up as debugging, and the house wins more than you do.

Here is the reframe that changes everything: treat a bug as a crime scene rather than a mystery. The evidence is already there. The error message, the terminal output, the Network tab, the stack trace, all of it is the program telling you exactly what went wrong and where. Debugging is just reading that evidence in order instead of panicking and contaminating it.

This guide sets up the tools and the habit so that next time something breaks, you have a process you can run every single time.

Read the error before you do anything

Before you touch a single tool, read the error message. Out loud if you have to. Most beginners skip this and jump straight to copy-paste, which is like calling a plumber before checking if the tap is turned on.

A decent error message hands you four things:

  • The type. TypeError, SyntaxError, ECONNREFUSED. This is the category of what went wrong.
  • The message. The human-ish sentence, like Cannot read properties of undefined (reading 'name'). This is usually the actual answer.
  • The file. Which file blew up.
  • The line. Which line in that file.

Take a real one:

TypeError: Cannot read properties of undefined (reading 'name')
    at getUser (/app/src/users.js:14:22)
    at /app/src/routes.js:8:19

Read those four parts and you already know: something on line 14 of users.js tried to read .name from a value that was undefined. You have not fixed it yet, but you know where to look and what to look for. That is most of the battle, and you did it without searching anything.

console.log: the honest workhorse

console.log is the duct tape of debugging. Older devs sometimes sneer at it, then quietly use it forty times a day. It is fine. Start here when you just want to know “what is this value at this point.”

The one rule: log a label with the value, never the value alone.

// Useless: which one of these is which?
console.log(user);
console.log(token);

// Useful: you can actually read the output
console.log("user:", user);
console.log("token:", token);

When you have a screen full of bare values and no labels, you have made a second debugging problem on top of your first one. Label them.

console.log shines for quick “is this even running” and “what is in this variable” checks. It gets clumsy when you need to inspect ten variables across a loop, or step through logic line by line. That is where breakpoints take over, and we will get there.

One discipline thing: remove your logs when you are done, or at least before you commit. A production app quietly printing console.log("HEREEEE", user) to its logs is a small embarrassment that lives forever in the git history.

Browser devtools: Console and Elements

If your bug is in the frontend (a React, Astro, or Angular page in the browser), open the developer tools. F12, or right-click and choose Inspect. Two panels matter to start.

The Console is where JavaScript errors and your console.log output show up. A blank page that “should be working” almost always has a red error sitting in the Console explaining itself. Look there first. A blank page with a clean Console is a different bug (often data or CSS), and now you know that too.

The Elements panel shows the live DOM, the actual HTML the browser built, which is frequently not the HTML you think you wrote. DOM stands for Document Object Model, which is a fancy way of saying “the page as a tree of elements the browser is currently holding in memory.” If your text is missing or your layout is wrong, inspect the element. You will see whether it rendered at all, what classes it actually has, and which CSS rule is winning. “Why is this button invisible” is usually answered in about ten seconds here, often by a display: none you did not expect.

The Network tab: where “the API is not working” gets solved

This is the panel beginners ignore the longest and need the most. API stands for Application Programming Interface, and for our purposes it just means your frontend asking your backend (or some other server) for data over HTTP.

When someone says “the API is not working,” they are almost never looking at the Network tab. So let us look at it.

Open devtools, click the Network tab, and reload or trigger the action. You get a list of every request the page made. Find the one that failed (it is usually red, or the one matching your broken feature) and click it. Now read four things in order.

What to readWhere it isWhat it tells you
Status codeTop of the request, or the Status columnWhether it worked, and how it failed
Request URLThe Headers sectionWhether you called the address you meant to
HeadersThe Headers sectionWhether auth and content-type were sent
Response bodyThe Response or Preview tabWhat the server actually said back

The status code alone narrows it down fast:

  • 404 Not Found. You called a URL that does not exist. Check the Request URL for a typo, a missing /api prefix, or the wrong port. This is the most common one and the least dramatic.
  • 401 Unauthorized (or 403 Forbidden). An auth problem. Your token or session is missing, expired, or not being sent. Check the request Headers for the Authorization header you expected to be there.
  • 500 Internal Server Error. The request reached your backend and your backend fell over. The frontend is fine. Stop debugging the browser and go read your server logs, because the real error is over there.
  • A CORS error. The request was blocked by the browser’s Cross-Origin Resource Sharing rules, which decide whether a page on one origin is allowed to call a server on another. You will see a specific CORS message in the Console. This is a server configuration fix (the server has to send the right Access-Control-Allow-Origin header), not something you can patch from the frontend.

Matching the request you sent to the response you got back is the entire game for API bugs. The Network tab shows you both halves at once.

Backend logs: your server is talking, are you listening

When the bug is on the server (an Express route, a database call, anything that runs in Node), the evidence is in your backend logs. That is just the text your server prints as it runs.

If you started your server in a terminal with something like node server.js or npm run dev, the logs are right there in that terminal window. This is its stdout, short for standard output, the default stream a program writes to. Errors and stack traces print here. Keep that window visible while you reproduce the bug and watch what shows up the moment it breaks.

If your server runs inside a container, the logs are not in your terminal, they are inside Docker. You pull them out with:

# Show the logs for a running container, and keep streaming new ones
docker logs -f my-api-container

The -f flag means “follow,” so the output keeps streaming live instead of dumping once and stopping. If you are fuzzy on containers, the Docker without the buzzwords guide explains where a container’s output actually goes and why it is not in the terminal you expected.

Stack traces: read them top to bottom

When something throws, you get a stack trace: the chain of function calls that led to the explosion, printed newest first. People see the wall of text and scroll past it. Do not. It is a map straight to the scene.

TypeError: Cannot read properties of undefined (reading 'email')
    at sendWelcome (/app/src/email.js:22:18)
    at registerUser (/app/src/users.js:41:5)
    at /app/src/routes/auth.js:12:11
    at Layer.handle (/app/node_modules/express/lib/router/layer.js:95:5)

Read it from the top. The top frame, email.js:22, is where it actually threw. That is your first place to look.

Now scan down for the first line that points at your code instead of node_modules. Here, the top three are your files and the fourth is deep inside Express. The frames inside node_modules are almost never the bug, they are library code doing its job with the bad input you handed it. The first line naming a file you wrote is the one to open. Click it (in VS Code, in a terminal that supports it, or in the browser Console) and you land exactly where the trouble started.

Breakpoints and the VS Code debugger

console.log answers “what is this value here.” A breakpoint answers “let me pause the whole program right here and poke at everything.” Once you get comfortable with breakpoints, you log a lot less.

A breakpoint is a marker that tells the program to freeze on a specific line so you can look around. In VS Code, you set one by clicking in the gutter just left of a line number. A red dot appears. When execution hits that line, the program pauses and you can inspect every variable in scope, see the call stack, and step forward one line at a time to watch values change.

That last part is the magic. Instead of guessing where a value goes wrong and sprinkling logs to triangulate it, you stop at the start and walk forward, watching the exact line where user turns into undefined. The bug stops being a theory.

To pause a backend process this way, VS Code needs to know how to start your app with the debugger attached. That is what a launch config is for.

launch.json: telling the debugger how to start your app

launch.json is a small file VS Code reads to know how to launch (or attach to) your program for debugging. It lives in a .vscode folder at the root of your project. Open the Run and Debug panel, click “create a launch.json file,” and you get something you can trim down to this:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug server",
      "program": "${workspaceFolder}/server.js",
      "skipFiles": ["<node_internals>/**"]
    }
  ]
}

Reading it line by line, because a config you cannot read is just a spell you hope works:

  • "type": "node" tells VS Code this is a Node.js program.
  • "request": "launch" means VS Code starts the app itself (as opposed to "attach", which connects to a process you already started).
  • "name" is just the label you pick in the dropdown.
  • "program" is the entry file to run, here server.js. The ${workspaceFolder} part is a variable VS Code fills in with your project root, so the path works on any machine.
  • "skipFiles" keeps the debugger from stepping into Node’s own internal code, so you stay in your files.

Now set a breakpoint in your route handler, hit F5, and trigger the request. Execution stops on your line with every variable laid out in front of you. No logs, no guessing, no rebuilding.

Debugging by bug type

The tool you reach for depends on where the bug lives. Here is the quick mapping.

Kind of bugSymptomWhere the evidence is
FrontendBlank page, wrong UI, dead buttonDevtools Console (errors) and Elements panel (the DOM)
Backend500 error, crash on a requestServer stdout, or docker logs, plus a breakpoint in the handler
API”It is not loading the data”Network tab on the frontend, matched against the backend logs
Environment / configA value is undefined for no reasonCheck the env vars: missing, misnamed, or not loaded

That last row catches a lot of people. When a value is undefined and the code looks correct, suspect an environment variable before you suspect your logic. A .env key that is misspelled, missing in production, or named API_URL in one place and API_BASE_URL in another will hand you a silent undefined and a confusing afternoon. If env vars are still fuzzy, read Environment variables explained, because “it works locally but the value is undefined in production” is almost always a missing or misnamed variable on the host.

The checklist to run when something breaks

When the panic instinct kicks in, run this in order instead. It moves from “what broke” outward to “where does it live,” and it stops you from fixing the wrong half of the app.

  1. What is the exact error, and where? Read the type, message, file, and line. Do not paste anything yet.
  2. Is the request even being made? Open the Network tab. Did the call fire? What is the status code (404, 401, 500, CORS)? Read the URL and the response body.
  3. Are the env vars present? Is anything undefined that should hold a value? Check for missing or misnamed keys, locally and in production.
  4. Does the stack trace point at my code? Read it top to bottom. Open the first frame that names a file you wrote, not one inside node_modules.
  5. Can I reproduce it? Can you make it fail on demand? A bug you can trigger reliably is a bug you can fix. An intermittent one means you have not found the real cause yet, so keep narrowing.

Five steps. Most bugs surrender somewhere in the first three.

Classic traps that waste your afternoon

The same handful of mistakes eat hours from people who skipped the process.

TrapWhat it looks likeThe actual move
Pasting the error before reading itTrying random fixes from old answersRead the four parts first, then search if needed
Ignoring the Network tab”The API is broken” with no idea whyOpen it, read the status code and response
Debugging the wrong sidePoking the frontend on a 500 errorA 500 is the backend; go read server logs
Skipping the stack traceEditing files at randomThe top frame in your code is the line to open
Bare console.logA screen of values you cannot tell apartAlways log a label with the value
Blaming logic for a config bugRewriting correct code over and overA stray undefined? Check env vars first

Why a calm process is what lets you ship

Here is the part nobody tells beginners: things will break. Your build will fail. A deploy will go sideways at the worst moment. This is not a sign you are bad at this, it is just Tuesday. Every developer you admire has stared at a red terminal at 11pm.

The difference between someone who freezes and someone who ships is not that the second person writes code that never breaks. It is that when it breaks, they have a process. They read the error. They check the Network tab. They open the right log. They walk the stack trace to their own file. The fear drains out of it because they are reading evidence instead of guessing in the dark.

That calm is exactly what lets you push to production without dread. When you trust that any break is just a crime scene you know how to read, the broken build stops being a threat and becomes a five-minute detour. Set up your devtools, write a launch.json, learn to read a stack trace, and the next time your project falls over on the way to shipping, you will fix it and keep moving.