Debugging with AI: errors, logs and stack traces
Learn to read a Python traceback and a browser error, gather the right evidence, and give AI a debugging prompt that finds the cause instead of guessing. You will fix two real Brew & Bean bugs and lock each fix in with a test.
In this tutorial you will fix two real bugs in the Brew & Bean project: a FastAPI order endpoint that crashes with 500 Internal Server Error, and a React menu page that shows a blank screen. Along the way you will learn the skill that matters more than any single fix: collecting evidence and handing it to an AI assistant so it can reason about the cause.
Prerequisites: Python 3.10 or newer with fastapi, uvicorn and pytest installed, and Node.js for the React part. It helps if you have done *Build the API with FastAPI*, but the code here is complete on its own. Any free assistant works: ChatGPT, Gemini, Claude, Copilot Chat in VS Code.
Most beginners debug with AI like this: paste the error, say "fix it", paste the next error, say "fix it" again. Sometimes it works. Often the AI guesses, changes five things at once, and you end up with new bugs and no idea what happened.
Professional debugging is a loop: reproduce the bug, read the evidence, form a hypothesis, test it, then fix and prevent it from coming back. AI is excellent at the middle part (reading evidence and suggesting hypotheses) if you give it good evidence.
The debugging loop
Follow this every time
- 1Reproduce the bug reliably. Write down the exact input or clicks that cause it. A bug you can't reproduce, you can't confirm you've fixed.
- 2Read the full error: the message, the type and the stack trace. Note the first line that points to *your* file, not a library.
- 3Locate the code at that line and look at the values involved. Add a log line or print if you don't know a value.
- 4Hypothesise: say in one sentence what you think is wrong. This is where AI helps most.
- 5Test the hypothesis with the smallest change or one extra log line before rewriting anything.
- 6Fix the cause, not the symptom, then add a test that would have caught the bug.
Bug 1: the order API returns 500
Here is a small version of the Brew & Bean order endpoint. It has a bug. Create a folder, save this as main.py, and run it with uvicorn main:app --reload.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
MENU = {
"espresso": {"name": "Espresso", "price": 120},
"cappuccino": {"name": "Cappuccino", "price": 180},
"cold-brew": {"name": "Cold Brew", "price": 220},
"filter-coffee": {"name": "Filter Coffee", "price": 80},
"masala-chai": {"name": "Masala Chai", "price": 90},
}
class OrderItem(BaseModel):
item_id: str
qty: int
class OrderIn(BaseModel):
customer_name: str
phone: str
items: list[OrderItem]
def compute_total(items: list[OrderItem]) -> int:
total = 0
for item in items:
total += MENU[item.item_id]["price"] * item.qty
return total
@app.post("/orders")
def create_order(order: OrderIn):
total = compute_total(order.items)
return {"customer_name": order.customer_name, "total": total, "status": "received"}A cut-down order endpoint. The server computes the total from the menu, as it should.
Now send two orders. The first uses a correct item_id. The second sends the display name "Cold Brew" instead of the id cold-brew, which is exactly the kind of mistake a frontend makes.
curl -X POST http://127.0.0.1:8000/orders \
-H "Content-Type: application/json" \
-d '{"customer_name": "Asha", "phone": "9876543210", "items": [{"item_id": "espresso", "qty": 2}]}'
curl -X POST http://127.0.0.1:8000/orders \
-H "Content-Type: application/json" \
-d '{"customer_name": "Asha", "phone": "9876543210", "items": [{"item_id": "Cold Brew", "qty": 1}]}'On Windows PowerShell, JSON quoting with curl is awkward. Open http://127.0.0.1:8000/docs instead and use the "Try it out" button on POST /orders.
{"customer_name":"Asha","total":240,"status":"received"}
Internal Server ErrorThe client only sees Internal Server Error. That message is deliberately vague: servers should not leak details to users. The real evidence is in the terminal where uvicorn is running.
ERROR: Exception in ASGI application
Traceback (most recent call last):
... (about 20 lines inside starlette, fastapi and anyio) ...
File "C:\brew-api\main.py", line 35, in create_order
total = compute_total(order.items)
File "C:\brew-api\main.py", line 29, in compute_total
total += MENU[item.item_id]["price"] * item.qty
~~~~^^^^^^^^^^^^^^
KeyError: 'Cold Brew'How to read a stack trace
A Python traceback lists the chain of function calls that led to the crash, oldest first. So you read it from the bottom up.
The last line is the error type and message: KeyError: 'Cold Brew'. A KeyError means you looked up a key that isn't in a dictionary. The lines just above it show where: main.py, line 29, in compute_total, and the ~~~~^^^^ markers (Python 3.11 and newer) point at the exact lookup that failed. One frame further up shows who called it: create_order on line 35.
Skip the frames inside starlette, fastapi or anyio. The bug is almost never in a popular library; it is in the first frame that belongs to your code.
Common errors and what they usually mean
| Error | Usually means | First thing to check |
|---|---|---|
Python KeyError: 'x' | Dictionary has no key x | Print the dict's keys and the key you used |
Python AttributeError: 'NoneType' object has no attribute 'y' | A variable you expected to hold an object is None | Where was it assigned? Did a lookup return nothing? |
Python ModuleNotFoundError | Package not installed in the active environment | Is your virtual environment activated? |
FastAPI 422 Unprocessable Entity | Request body doesn't match the Pydantic model | Read the detail list: it names the field |
JS TypeError: Cannot read properties of undefined (reading 'map') | Calling .map on something that is still undefined | Initial state, or the API returned a different shape |
Browser blocked by CORS policy | Backend doesn't allow the frontend's origin | CORS settings on the server, not the frontend code |
HTTP 404 Not Found from your own API | Wrong URL, method or route path | Compare the URL with the routes in /docs |
Ask AI for the cause, not just a fix
Now give the evidence to your assistant. A good debugging prompt contains four things: the code around the error, the full error message, what you expected, and what actually happened. Then it asks for an explanation and ranked hypotheses before any code.
I'm debugging a [language/framework, e.g. Python FastAPI] app. Help me find the root cause before suggesting a fix.
What I did: [exact steps or request that triggers the bug]
What I expected: [expected result]
What happened: [actual result]
Full error and stack trace:
---
[paste the complete error, including the last lines]
---
Relevant code:
---
[paste the function(s) named in the stack trace]
---
What I've already tried: [or "nothing yet"]
Please:
1. Explain in plain English what the error means.
2. List the 2-3 most likely causes, most likely first, and how I can confirm each one (a print, a log line, a test).
3. Only then suggest the smallest fix for the most likely cause, and say what it does NOT handle.What to expect: A good answer explains the error type, points to the exact line, gives checks you can run, and proposes a small, targeted change. Be suspicious if it rewrites the whole file or wraps everything in try/except.
For this bug, a good answer says: the frontend sent "Cold Brew", but MENU is keyed by ids like "cold-brew", so the lookup fails. It should also point out the deeper issue: the API trusts that every item_id exists, so any unknown id crashes the server. The right fix is to check the id and return a clear 400 Bad Request, not to add "Cold Brew" as an extra key.
from fastapi import FastAPI, HTTPException
# ... MENU, OrderItem and OrderIn stay the same ...
def compute_total(items: list[OrderItem]) -> int:
total = 0
for item in items:
menu_item = MENU.get(item.item_id)
if menu_item is None:
raise HTTPException(status_code=400, detail=f"Unknown item_id: {item.item_id}")
total += menu_item["price"] * item.qty
return totaldict.get returns None instead of raising. We turn a server crash into a clear client error that the frontend can show.
Fixes that hide bugs
AI assistants sometimes "fix" an error by making it disappear: wrapping code in try: ... except: pass, returning a default total of 0, or deleting the line that failed. The error is gone, but the bug is now silent, which is worse. Ask: does this fix the cause, or does it just stop the message?
Lock the fix in with a test
A fix without a test can come back next week when you or the AI edit this function again. Write a regression test: a test that reproduces the original bug and checks the correct behaviour. Install the test tools with pip install pytest httpx (FastAPI's TestClient needs httpx).
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_unknown_item_returns_400():
response = client.post(
"/orders",
json={
"customer_name": "Asha",
"phone": "9876543210",
"items": [{"item_id": "Cold Brew", "qty": 1}],
},
)
assert response.status_code == 400
assert response.json() == {"detail": "Unknown item_id: Cold Brew"}
def test_valid_order_total():
response = client.post(
"/orders",
json={
"customer_name": "Asha",
"phone": "9876543210",
"items": [{"item_id": "cold-brew", "qty": 2}],
},
)
assert response.status_code == 200
assert response.json()["total"] == 440Run with pytest -q in the same folder as main.py.
.. [100%]
2 passed in 1.12sCheck the test fails first
Temporarily undo your fix and run pytest again. The first test should now fail. If a test passes with and without the fix, it isn't testing the bug.
Bug 2: a blank React page
Frontend bugs often show nothing at all: the page is just white. The evidence is in the browser's DevTools (press F12, or right-click and choose Inspect). Here is a buggy menu page.
import { useEffect, useState } from "react";
export default function MenuPage() {
const [menu, setMenu] = useState();
useEffect(() => {
fetch("http://localhost:8000/menu")
.then((res) => res.json())
.then((data) => setMenu(data));
}, []);
return (
<ul>
{menu.map((item) => (
<li key={item.id}>
{item.name}: ₹{item.price}
</li>
))}
</ul>
);
}Renders the menu from GET /menu. It crashes on the very first render.
Uncaught TypeError: Cannot read properties of undefined (reading 'map')
at MenuPage (MenuPage.jsx:14)Read it the same way: the error type and message first, then the first line that points to your file. menu is undefined at line 14. Why? React renders the component once before useEffect runs, and useState() with no argument starts as undefined. The fetch hasn't even started yet.
The fix is to start with an empty list and handle loading and errors, so the page never depends on the data having arrived.
import { useEffect, useState } from "react";
export default function MenuPage() {
const [menu, setMenu] = useState([]);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("http://localhost:8000/menu")
.then((res) => {
if (!res.ok) throw new Error(`Server returned ${res.status}`);
return res.json();
})
.then((data) => setMenu(data))
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}, []);
if (loading) return <p>Loading menu...</p>;
if (error) return <p>Could not load the menu: {error}</p>;
return (
<ul>
{menu.map((item) => (
<li key={item.id}>
{item.name}: ₹{item.price}
</li>
))}
</ul>
);
}Starts with an empty array, checks the HTTP status, and shows loading and error states instead of a white page.
If the page now says "Could not load the menu: Failed to fetch", open the Network tab in DevTools, reload, and click the menu request. Check three things: the URL, the status code, and the response body. If the Console shows has been blocked by CORS policy, the browser refused to read the response because the backend did not allow the frontend's origin (http://localhost:5173 for Vite). That is fixed on the server with FastAPI's CORSMiddleware, as shown in *Connect the website to the API and deploy*.
When the error message isn't enough: logs
Some bugs don't crash. The total is wrong, or an order gets the wrong status. Then you need logs: lines your code writes so you can see the values as it runs. A quick print() is fine while debugging locally. For anything you keep, use Python's logging module so each line has a time and a level.
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("brew_api")
@app.post("/orders")
def create_order(order: OrderIn):
logger.info("New order: %d items for %s", len(order.items), order.customer_name)
total = compute_total(order.items)
logger.info("Computed total: %d", total)
return {"customer_name": order.customer_name, "total": total, "status": "received"}Prints lines like INFO:brew_api:Computed total: 440 in the uvicorn terminal.
Don't paste secrets or personal data into AI chats
Before pasting logs or code into a chat, remove API keys, passwords, database URLs and real customers' phone numbers. Replace them with fakes like sk-XXXX or 9999999999. Logs from a real app can contain personal data protected under India's DPDP Act. Also avoid logging full phone numbers in the first place.
Here are log lines from my [app type] around the time a bug happened. The bug: [describe what went wrong, e.g. an order total was ₹0].
---
[paste log lines, with secrets and personal data removed]
---
What do these lines tell us about what the code did? Which value looks wrong first? What one extra log line would you add, and where, to narrow it down?What to expect: Good answers point to a specific suspicious value and suggest one targeted log line, not a full rewrite.
When AI goes in circles
Sometimes each AI fix causes a new error, and after five rounds the code is a mess. Stop. Signs you are in a loop: the AI repeats an earlier suggestion, the file keeps growing, or you no longer understand the code.
What to do: undo to the last working version (this is why you commit often with Git: git diff shows what changed and git restore main.py throws away uncommitted edits). Start a fresh chat, because a long chat full of wrong guesses keeps steering the model. Give it a fresh, complete prompt with the current error. And try explaining the bug in your own words first: often you spot the cause while writing it out, a habit called rubber-duck debugging.
Try it: Break it on purpose
Using the fixed main.py, trigger and debug three more problems. For each one, write down the error, the line it points to, and your hypothesis before you ask AI.
1. Send an order with "qty": "two".
2. Send an order with "items": [].
3. Send an order with "qty": -3 for espresso.
Then add a test for each case to test_orders.py.
Before you ask AI to debug
- I can reproduce the bug with exact steps or input.
- I copied the full error, including the last lines of the stack trace.
- I found the first stack frame in my own code.
- I included the relevant code, what I expected and what happened.
- I removed keys, passwords and personal data.
- I asked for the cause and how to confirm it, not just a fix.
Key takeaways
- Debug in a loop: reproduce, read, locate, hypothesise, test, fix, then add a regression test.
- Read Python tracebacks bottom-up and focus on the first frame in your own code.
- Frontend evidence lives in DevTools: the Console for errors, the Network tab for requests and responses.
- Give AI the error, the code, expected vs actual behaviour, and ask for causes before code.
- Reject fixes that hide errors (empty except blocks, default values) instead of fixing the cause.
- If AI goes in circles, revert, start a fresh chat and explain the bug in your own words.
Check yourself
Finishing the quiz marks the lesson as done.
1. A Python traceback has 25 lines, mostly inside starlette and anyio. Where should you look first?
2. Your API returns Internal Server Error to curl. Where is the useful evidence?
3. An AI suggests fixing a KeyError by wrapping the loop in try: ... except: pass. What's the problem?
4. A React page shows Cannot read properties of undefined (reading 'map') on load. What is the most likely cause?
5. Why write a regression test after fixing a bug?
6. After five rounds of AI fixes, each one causes a new error. What's the best next step?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.