How to read a stack trace: a crashing orders page on day one
Learn to read a stack trace through a real bug: read the error message, find your own line of code, trace the bad data back and fix it where it starts.
Staring at a screen full of red text and not sure where to start? Reading a stack trace takes four steps: read the whole error message, find the first line that belongs to your code, trace back where the bad data came from, and only then fix it. I'll walk you through all four with one concrete bug.
A message at 7 a.m.
This is the first post in the series Taking over a legacy project. In it, you play a developer who has just taken over the order system of Mộc, a small coffee shop in Vietnam that sells online. Mộc is made up for this series, but the system has real code that runs. The person who wrote it has left the company, and they left almost no documentation behind.
On your first morning, the owner sends you a message:
Hi, the order list page won't open anymore. It just says "Server error". We haven't been able to see a single order all morning.
That's the whole message. No screenshot, no idea when it started, no idea who changed what. Once you start working, you'll get messages like this all the time: the user can only describe the symptom, and they need it fixed now.
The shop's system is small. To run the code in this post you need Node.js 22.18 or later, because from that version on Node.js runs TypeScript files directly without installing any extra tools. Create a folder with these three files and open it in VS Code:
moc-orders/
data/orders.json # the list of orders
src/orders.ts # order helpers: customer name, total, the list
src/server.ts # server that returns the order list at http://localhost:3000/ordersThe first file holds the order helpers. The Order type describes one order. customerLabel returns the customer's name in capitals, orderTotal adds up the price, and listOrders puts both together for every order. Copy it line for line, because the stack trace later on points at line numbers in this file:
export type Customer = { name: string; phone: string };
export type OrderItem = { name: string; price: number; quantity: number };
export type Order = {
id: number;
createdAt: string;
source: "web" | "old-pos";
customer: Customer;
items: OrderItem[];
};
export function customerLabel(order: Order): string {
return order.customer.name.toUpperCase();
}
export function orderTotal(order: Order): number {
return order.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
export function listOrders(orders: Order[]) {
return orders.map((order) => ({
id: order.id,
customer: customerLabel(order),
total: orderTotal(order),
}));
}The second file is the server. It reads the JSON file once at startup. Whenever someone opens /orders, it calls listOrders and sends back the result. If something throws, it prints the error details to the terminal and answers "Server error" to the user:
import { readFileSync } from "node:fs";
import { createServer } from "node:http";
import { listOrders, type Order } from "./orders.ts";
const dataFile = new URL("../data/orders.json", import.meta.url);
const orders: Order[] = JSON.parse(readFileSync(dataFile, "utf8"));
export const server = createServer((req, res) => {
if (req.url === "/orders") {
try {
const body = JSON.stringify(listOrders(orders), null, 2);
res.writeHead(200, { "content-type": "application/json; charset=utf-8" }).end(body);
} catch (error) {
console.error(error);
res.writeHead(500, { "content-type": "text/plain; charset=utf-8" }).end("Server error");
}
return;
}
res.writeHead(404).end();
});
const port = Number(process.env.PORT ?? 3000);
server.listen(port, () => console.log(`Moc orders: http://localhost:${port}/orders`));The last file is the data: four orders from one morning. The first two came in through the website. The other two were entered on the shop's old point-of-sale machine at the counter ("source": "old-pos"). Prices are in Vietnamese đồng, so 29,000 is a little over one US dollar. Copy the file as it is, but don't study it yet, because this whole post is about finding out what's wrong with it:
[
{
"id": 1041,
"createdAt": "2026-11-03T08:12:00+07:00",
"source": "web",
"customer": { "name": "Nguyễn An", "phone": "0901000001" },
"items": [
{ "name": "Iced milk coffee", "price": 29000, "quantity": 2 },
{ "name": "Egg banh mi", "price": 25000, "quantity": 1 }
]
},
{
"id": 1042,
"createdAt": "2026-11-03T09:40:00+07:00",
"source": "web",
"customer": { "name": "Trần Bình", "phone": "0901000002" },
"items": [{ "name": "Bac xiu", "price": 32000, "quantity": 1 }]
},
{
"id": 1043,
"createdAt": "2026-11-03T10:05:00+07:00",
"source": "old-pos",
"items": [{ "name": "Black coffee", "price": 25000, "quantity": 1 }]
},
{
"id": 1044,
"createdAt": "2026-11-03T10:30:00+07:00",
"source": "old-pos",
"customer": { "name": "Lê Chi", "phone": "0901000004" },
"items": [{ "name": "Salt coffee", "price": "35.000", "quantity": 2 }]
}
]Why make the error happen on your machine first?
When a bug comes in, everyone wants to open the code and start reading. The first thing to do, though, is to make the error happen on your own machine. Until you've seen it with your own eyes, you have no way of knowing for sure that you've fixed it.
Open two terminal windows. The first one runs the server, the second one calls the order list page:
node src/server.tscurl -i http://localhost:3000/ordersThe second terminal shows exactly what the owner described: status 500 and the text "Server error". That's as far as a customer can see. You, on the other hand, switch to the first terminal, where the server printed the details. I've shortened the paths so they're easier to read. The start of each path will be different on your machine, but the file names, line numbers and column numbers will be the same:
Moc orders: http://localhost:3000/orders
TypeError: Cannot read properties of undefined (reading 'name')
at customerLabel (file:///…/moc-orders/src/orders.ts:12:25)
at file:///…/moc-orders/src/orders.ts:22:15
at Array.map (<anonymous>)
at listOrders (file:///…/moc-orders/src/orders.ts:20:17)
at Server.<anonymous> (file:///…/moc-orders/src/server.ts:12:35)
at Server.emit (node:events:519:28)
at parserOnIncoming (node:_http_server:1186:12)
at HTTPParser.parserOnHeadersComplete (node:_http_common:125:17)This block of text is the stack trace: the list of functions that were still running at the moment the error happened. Eight lines starting with at look intimidating, but you don't need to understand every one of them. You only need to read them in the right order.
The server can print these lines because the catch block in server.ts calls console.error(error). Plenty of old systems catch errors and then print nothing at all, so the error gets swallowed. If your terminal is empty, find that catch block and add a line that prints the error before you do anything else.
What are the parts of a stack trace?
The first line, TypeError, tells you the kind of error, and that already hints at where to look. TypeError means the code used a value of the wrong type, ReferenceError means it used a variable that was never declared, and SyntaxError means the code itself is malformed. Right after the error type comes the error message. Then come the at lines, one per function, each with that function's location in the form file:line:column.
In JavaScript, the top at line is where the error happened, and each line below it is the function that called the line above. So if you read from the bottom up, you see the whole path the program took. The server receives a request and calls listOrders. listOrders calls map, map calls an arrow function, the arrow function calls customerLabel, and the error happens inside customerLabel.
Step 1: read the whole error message
When they see red text, beginners tend to jump straight into the code. Try reading this message slowly first:
Cannot read properties of undefined (reading 'name')
It says the program couldn't read a property of a value that is undefined, and the property it was reading is name. In other words, somewhere in the code there is something like something.name, and something is undefined. You haven't opened a single file yet, and you already know what to look for.
When you hit an error message you don't recognize, look it up in MDN's list of JavaScript errors. Each error there comes with an explanation and examples, which is far more reliable than the first forum answer you happen to find.
Step 2: find the first line that is your code
Go from the top down and cross out every line that isn't your project's code. Array.map (<anonymous>) is a built-in JavaScript function. The node:events and node:_http_server lines belong to Node.js itself. Any path containing node_modules is library code. You can skip all of those.
What's left are four lines from the project, and the first of them is customerLabel in src/orders.ts, line 12, column 25. Open the file:
export function customerLabel(order: Order): string {
return order.customer.name.toUpperCase();
}Column 25 points right at .name. Put that together with what step 1 told you, and you can conclude that order.customer is undefined.
One small detail helps here. When Node.js runs a TypeScript file, it replaces the type annotations with spaces instead of squeezing the code together. That's why the line and column numbers in the stack trace match the .ts file you have open. With other build tools you need a source map to get the same result, and I'll come back to source maps at the end of this post.
Step 3: where did the bad data come from?
At this point many people change the line to order.customer?.name, see the page work again, and close the ticket. The error does go away, but nobody has answered the question "why does an order have no customer?". Line 12 is only where the error showed up. Where it started is somewhere else.
To find where it started, walk back up the at lines and ask who passed this value in. customerLabel gets order from the arrow function on line 22. That arrow function is called by map for every order inside listOrders. listOrders gets the orders from server.ts, and server.ts reads them from data/orders.json. So the right question now is: which order in the JSON file has no customer?
Instead of scanning the file by eye, ask the data directly:
node -e 'const orders = require("./data/orders.json"); console.log(orders.filter((o) => !o.customer).map((o) => ({ id: o.id, source: o.source })))'[ { id: 1043, source: 'old-pos' } ]There's exactly one, and it came from old-pos, the shop's old point-of-sale machine. When you ask, the owner explains that the old machine is used for walk-in customers at the counter, and staff don't enter a name or phone number for them. So an order without a customer is perfectly normal, and the data isn't broken at all.
What's actually wrong is this type definition:
export type Order = {
id: number;
createdAt: string;
source: "web" | "old-pos";
customer: Customer;
items: OrderItem[];
};The Order type claims that every order has a customer, while the real data says otherwise. TypeScript doesn't complain, because TypeScript only checks your code while you write it. Data read from a file while the program runs is checked by nobody, and JSON.parse returns whatever is in the file. In other words, the type described reality wrongly, and the compiler believed it.
Where should the fix go?
You have three options: add ?. on line 12, delete order 1043 and move on, or make the Order type match reality and handle orders without a customer.
Deleting data is off the table, because that's a real order. Adding ?. sounds reasonable, so let's try it and look at the result. Change line 12 to order.customer?.name.toUpperCase(), call the page again and look at order 1043:
{
"id": 1043,
"total": 25000
}The page no longer crashes, and the total is still right. But the customer field is gone entirely, not even an empty string. That's because order.customer?.name returns undefined, and JSON.stringify silently drops every property whose value is undefined.
The shop's front end always expects a customer field, so now it will show an empty cell or the word "undefined", depending on how that code was written. The bug hasn't gone away. It has moved somewhere harder to see.
It doesn't stop there. Say that next month someone builds a feature that texts customers a reminder. They open the Order type, see that customer is always there, and write order.customer.phone:
Order 1043: Cannot read properties of undefined (reading 'phone')The same error shows up in a different place, caused by a different person, who now has to read the stack trace from scratch. That's the price of patching where the error shows up. So I went with the third option. Put line 12 back the way it was (remove the ?.), then replace the Order type and the customerLabel function in src/orders.ts with the code below. orderTotal and listOrders stay as they are:
export type Order = {
id: number;
createdAt: string;
source: "web" | "old-pos";
// Walk-in orders from the old point-of-sale machine (source "old-pos") may have no customer.
customer?: Customer;
items: OrderItem[];
};
export function customerLabel(order: Order): string {
if (!order.customer) return "WALK-IN CUSTOMER";
return order.customer.name.toUpperCase();
}The biggest win is the ? after customer. From now on, any place in the project that reads order.customer without checking it first gets a TypeScript error, including next month's reminder feature, which would now fail with 'order.customer' is possibly 'undefined'. One character heads off a whole class of bugs that haven't happened yet.
The comment above it is worth as much as the code. Six months from now, whoever reads this code again (quite possibly you) will understand right away why a customer can be missing, and won't have to walk the path you just walked.
Run it again to check
Restart the server with the fixed code, then call the order list page again:
node src/server.ts
curl http://localhost:3000/orders[
{ "id": 1041, "customer": "NGUYỄN AN", "total": 83000 },
{ "id": 1042, "customer": "TRẦN BÌNH", "total": 32000 },
{ "id": 1043, "customer": "WALK-IN CUSTOMER", "total": 25000 },
{ "id": 1044, "customer": "LÊ CHI", "total": 70 }
]The page works again. You send the owner a quick message, and you write the cause down in the commit so the next person doesn't have to guess: the first line says what you fixed, and the body says why it needed fixing. There are more posts on habits like this in the programming skills topic.
What if the stack trace isn't this easy to read?
Today's bug was a friendly one: the stack trace was complete and pointed straight at the broken line. Real life isn't always like that.
The first case is a truncated stack trace. V8, the engine that runs JavaScript in Chrome and Node.js, keeps only 10 lines by default (Error.stackTraceLimit is 10). The stack trace above has only 8, so it's complete, but if the error happens deep inside a long chain of calls, the start of the chain gets cut off. While debugging on your machine, you can raise the limit temporarily:
Error.stackTraceLimit = 50;The second case is a lost original error. Old code often looks like this:
try {
await loadOrders();
} catch (err) {
throw new Error("Failed to load orders"); // the original error is gone
}Now the stack trace only points at the new throw. Nobody can tell whether loadOrders failed because the file doesn't exist, because of missing permissions, or because the JSON is malformed. If you pass the original error in the `cause` option, you keep both the new explanation and the original reason:
try {
await loadOrders();
} catch (err) {
throw new Error("Failed to load orders", { cause: err });
}The third case is production code that has been minified. Function names turn into a and b, and the whole file is squeezed onto line 1. You need a source map, a file that maps the minified code back to the original. Next.js, Vite and most build tools can generate source maps, and browsers and error tracking services use them to show the real file names and line numbers again.
The last case is when every at line is inside node_modules. Libraries are rarely wrong, so usually it's your code that passed bad data into the library. Find the last line that belongs to your code, just before the call goes into the library, and check the data there. If you debug step by step in the browser, Chrome DevTools ignores scripts in `/node_modules/` by default, so you won't get dragged into library code.
If you've done all four steps and still can't find the cause, that's the moment to ask someone. Bring the stack trace and what you've already tried. If you'd like to test yourself, try the JavaScript questions in the interview question bank.
And an order that costs 70 đồng
Look at the result from the check one more time. Order 1044, from Lê Chi, is two salt coffees, yet the total is only 70 đồng, which is less than a cent.
This time there is no error, no stack trace, and the page still returns status 200. The program runs fine and produces a wrong number. In my opinion this kind of bug is much nastier, because nothing points you in the right direction. The next post in the series uses breakpoints in VS Code to pause the program halfway and look at every value with your own eyes. You can follow the whole story in the series Taking over a legacy project.