How to Read Error Messages and Stack Traces
Real tracebacks from Python, PHP, JS, Java, Dart, Swift and C read line by line: which frame is yours, what Caused by means, and how to search an error.
Series: Testing & Debugging lecture 1/6 (beginner). For new developers who freeze when a red wall of text appears. After this lecture you will be able to find the one line that matters in a Python, PHP, JavaScript, Java, Dart, Swift or C error report, and to search for it in a way that actually finds answers.
If you just joined a team and your first on-call shift starts next week, this is the skill you need before any testing framework. A stack trace is not noise: it is the program telling you exactly where it was when it gave up. The trouble is that each language prints it in a different order, with different words for "this error was caused by that one", and with a lot of lines that are not about your code.
Everything below was run for real on an Apple Silicon Mac (M1 Pro): Python 3.9.6, PHP 8.5.6, JavaScriptCore (jsc, the engine that ships with macOS), OpenJDK 26, Dart 3.11.5, Swift 6.4.2 and Apple clang 21. Home-directory paths were shortened to ~/demo/; nothing else was edited unless marked with ….
Your first traceback in five minutes
A stack trace (Python calls it a traceback) is the list of function calls that were active when the error happened. Each entry is a frame: one function, one file, one line. Function main called order_total, which called line_total, which crashed: three frames.
Save this as orders.py, next to an order.json where the second item has no qty:
import json
import sys
def load(path):
with open(path) as f:
return json.load(f)
def line_total(item):
return item["price"] * item["qty"]
def order_total(order):
return sum(line_total(item) for item in order["items"])
def main(path):
order = load(path)
print(order["id"], order_total(order))
main(sys.argv[1])
{"id": "A-1001", "items": [{"name": "pen", "price": 120, "qty": 3}, {"name": "notebook", "price": 300}]}
$ python3 orders.py order.json
Traceback (most recent call last):
File "~/demo/orders.py", line 23, in <module>
main(sys.argv[1])
File "~/demo/orders.py", line 20, in main
print(order["id"], order_total(order))
File "~/demo/orders.py", line 15, in order_total
return sum(line_total(item) for item in order["items"])
File "~/demo/orders.py", line 15, in <genexpr>
return sum(line_total(item) for item in order["items"])
File "~/demo/orders.py", line 11, in line_total
return item["price"] * item["qty"]
KeyError: 'qty'
Read it in this order:
- The last line is the error type and message:
KeyError: 'qty'. AKeyErrormeans "this dictionary has no such key". The message is the missing key. - The header says
most recent call last. So the frame just above the error is where it failed:line_total, line 11. - Walk upwards only if you need to know how you got there:
order_totalcalled it from a generator expression (<genexpr>),maincalled that, and the module level calledmain.
The fix is not in line_total. The code is fine for valid orders; the data is not. Whether you reject the order, default qty to 1, or fix whoever wrote the JSON is a business decision. The traceback's job is only to point at the right line and the right key, and it did.
When the failing frame is not your code
Break the JSON instead (a trailing comma after the last item) and the traceback grows:
$ python3 orders.py bad.json
Traceback (most recent call last):
File "~/demo/orders.py", line 23, in <module>
main(sys.argv[1])
File "~/demo/orders.py", line 19, in main
order = load(path)
File "~/demo/orders.py", line 7, in load
return json.load(f)
File "…/lib/python3.9/json/__init__.py", line 293, in load
return loads(fp.read(),
File "…/lib/python3.9/json/__init__.py", line 346, in loads
return _default_decoder.decode(s)
File "…/lib/python3.9/json/decoder.py", line 337, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
File "…/lib/python3.9/json/decoder.py", line 355, in raw_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 68 (char 67)
The bottom four frames are inside the standard library. They are almost never wrong. The frame that matters is the newest one in your own file: orders.py, line 7, json.load(f). And the message tells you where in the input to look: column 68 of line 1 is the ] right after the stray comma.
This is the rule that works everywhere: skip frames from the standard library, frameworks and the runtime until you reach a path you wrote. In a Flask or Django app that can mean skipping thirty frames. The error happened in their code; the cause is almost always in what your code passed to it.
Chained exceptions: "caused by", "during handling" and "Next"
Real code catches low-level errors and re-raises them with context. Then you get two tracebacks in one report, and the order matters.
def parse_order(text):
try:
data = json.loads(text)
return data["id"], data["items"][0]["qty"]
except KeyError as e:
raise OrderError("order A-1001 is missing a field") from e
Traceback (most recent call last):
File "~/demo/chained.py", line 12, in parse_order
return data["id"], data["items"][0]["qty"]
KeyError: 'qty'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "~/demo/chained.py", line 27, in <module>
parse_order(text)
File "~/demo/chained.py", line 14, in parse_order
raise OrderError("order A-1001 is missing a field") from e
__main__.OrderError: order A-1001 is missing a field
raise … from e is a deliberate chain: the original KeyError comes first, then the wrapper. Now compare a bug inside the except block itself:
Traceback (most recent call last):
File "~/demo/chained.py", line 20, in parse_order_sloppy
return data["id"], data["items"][0]["qty"]
KeyError: 'qty'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "~/demo/chained.py", line 29, in <module>
parse_order_sloppy(text)
File "~/demo/chained.py", line 22, in parse_order_sloppy
log.warning("bad order: %s", text) # bug: 'log' was never defined
NameError: name 'log' is not defined
"During handling" means two bugs: the data problem, and an error handler that crashed (log was never defined). On-call engineers see this pattern a lot: the error-reporting path is the least-tested code in any system.
Java prints the wrapper first and the original after Caused by::
$ java Orders.java
Exception in thread "main" java.lang.IllegalArgumentException: bad qty in order A-1001
at Orders.loadQty(Orders.java:19)
at Orders.main(Orders.java:28)
Caused by: java.lang.NumberFormatException: For input string: "3 "
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:527)
at java.base/java.lang.Integer.parseInt(Integer.java:624)
at Orders.loadQty(Orders.java:17)
at Orders.main(Orders.java:28)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
… 4 more launcher frames
The real answer is inside the quotes: "3 " has a trailing space, and Integer.parseInt does not trim. In a long Java trace, scroll to the last Caused by:. That is the root.
PHP uses the word Next, with the original first:
$ php -d display_errors=0 orders.php bad
PHP Fatal error: Uncaught JsonException: Syntax error in ~/demo/orders.php:18
Stack trace:
#0 ~/demo/orders.php(18): json_decode('{"id":"A-1002",...', true, 512, 4194304)
#1 ~/demo/orders.php(28): loadOrder('{"id":"A-1002",...')
#2 {main}
Next RuntimeException: cannot parse order in ~/demo/orders.php:20
Stack trace:
#0 ~/demo/orders.php(28): loadOrder('{"id":"A-1002",...')
#1 {main}
thrown in ~/demo/orders.php on line 20
Note that PHP shows the arguments of each call, cut to 15 characters. That is often enough to spot bad input without a debugger. We passed -d display_errors=0 because the PHP CLI otherwise prints every fatal error twice, once to stderr through the log and once to stdout.
The same bug in seven runtimes
Here is what the "missing qty" bug looks like elsewhere. Notice which ones do not crash at all.
PHP 8.5 treats a missing array key as a warning and keeps going with null:
$ php -d display_errors=0 orders.php missing
PHP Warning: Undefined array key "qty" in ~/demo/orders.php on line 5
0
The total is 0, the exit code is 0, and the bug ships. Treat PHP warnings as errors in development.
JavaScript (jsc) quietly produces NaN for the missing field, then crashes on a missing object:
$ jsc orders.js
A-1001 total: NaN
Exception: TypeError: undefined is not an object (evaluating 'order.items.reduce')
orderTotal@orders.js:5:21
global code@orders.js:10:34
The format is function@file:line:column, newest first. The wording undefined is not an object is JavaScriptCore's (Safari's). V8 in Chrome and Node says Cannot read properties of undefined (reading 'reduce') for the same bug (not run here). When we threw new Error("…", { cause: e }), jsc's uncaught-error output did not print the cause; we had to print e.cause ourselves.
Java 26 names the exact null in its message ("helpful NullPointerExceptions"):
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null
at Orders.lineTotal(Orders.java:6)
at Orders.orderTotal(Orders.java:11)
at Orders.main(Orders.java:26)
Dart prints numbered frames, newest first, and the runtime's own frames at the bottom:
Unhandled exception:
type 'Null' is not a subtype of type 'int' in type cast
#0 lineTotal (file://~/demo/orders.dart:4:48)
#1 orderTotal (file://~/demo/orders.dart:10:12)
#2 main (file://~/demo/orders.dart:17:9)
#3 _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:314:19)
#4 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:193:12)
"Null is not a subtype of int" is the Dart way of saying "you cast a missing value to int". Flutter developers see this one daily with JSON.
Swift gives one line and exit code 133 (a trap):
$ ./orders_swift
orders_swift/orders.swift:4: Fatal error: Unexpectedly found nil while unwrapping an Optional value
To see the stack, run it under lldb and type bt (backtrace). Frames 0 to 3 are the Swift runtime reporting the error; frame 4 is yours, and lldb even shows the argument that broke it:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = Fatal error: Unexpectedly found nil while unwrapping an Optional value
frame #0: 0x000000019c55cec0 libswiftCore.dylib`_swift_runtime_on_report
frame #1: … libswiftCore.dylib`_swift_stdlib_reportFatalErrorInFile + 204
frame #2: … libswiftCore.dylib`closure #1 in closure #1 in _assertionFailure(_:_:file:line:flags:) + 400
frame #3: … libswiftCore.dylib`_assertionFailure(_:_:file:line:flags:) + 276
* frame #4: 0x0000000100000dd4 orders_swift`lineTotal(name="notebook", qty=1) at orders.swift:4:24
frame #5: 0x0000000100000ad4 orders_swift`orderTotal(items=2 values) at orders.swift:9:39
frame #6: 0x0000000100000830 orders_swift`main at orders.swift:13:7
frame #7: 0x0000000188a244e4 dyld`start + 6992
C is the scary one. A loop with i <= n_items reads one element past a 3-element malloc block. Built normally, it prints 500 and exits 0: the extra read happened to find a zero. Built with AddressSanitizer (-fsanitize=address -g), it stops at the first bad read:
==36413==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6020000000fc at pc 0x000102f2ca8c bp 0x00016ced2330 sp 0x00016ced2328
READ of size 4 at 0x6020000000fc thread T0
#0 0x000102f2ca88 in line_total orders.c:5
#1 0x000102f2c9dc in order_total orders.c:11
#2 0x000102f2c964 in main orders.c:18
#3 0x000188a244e0 in start+0x1b4c (dyld:arm64e+0x204e0)
0x6020000000fc is located 0 bytes after 12-byte region [0x6020000000f0,0x6020000000fc)
allocated by thread T0 here:
#0 0x0001038a5274 in malloc+0x78 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x41274)
#1 0x000102f2c82c in main orders.c:16
#2 0x000188a244e0 in start+0x1b4c (dyld:arm64e+0x204e0)
SUMMARY: AddressSanitizer: heap-buffer-overflow orders.c:5 in line_total
… 30 lines of shadow-byte map
The full report is 47 lines. You need three of them: the error type (heap-buffer-overflow), frame #0 of the first stack (orders.c:5), and "0 bytes after 12-byte region", which means "you read the element right after the end of a 3-int array". The second stack tells you where that array was allocated. The shadow-byte map at the bottom is for sanitizer developers; skip it.
A cheat sheet: which end is newest, and what the chain word is
| Runtime | Newest frame | Chain wording | First thing to read |
|---|---|---|---|
| Python | bottom (most recent call last) |
direct cause (from) / During handling (bug in handler) |
last line, then the frame above it |
| Java | top | Caused by: (original after wrapper) |
the last Caused by: block |
| PHP | top (#0) |
Next (wrapper after original) |
first line: type, file:line |
| JavaScript (jsc) | top | cause not printed when uncaught |
Exception: line, first fn@file:line:col |
| Dart | top (#0) |
none printed by default | line 2, then #0 |
| Swift | top in lldb bt |
none | the * frame in lldb |
| C + ASan | top (#0) |
none | SUMMARY: line and "N bytes after" |
And the errors you will meet in your first week, all copied from real runs:
| Runtime | Error you will see | Usual cause |
|---|---|---|
| Python | TypeError: can only concatenate str (not "int") to str |
"total: " + 3; use an f-string |
| Python | IndentationError: expected an indented block |
body of if/def not indented |
| Python | ModuleNotFoundError: No module named 'reqests' |
typo, or wrong virtualenv |
| PHP | Warning: Undefined variable $count |
typo; the script keeps running |
| PHP | Uncaught Error: Call to undefined function tota() |
typo or missing require |
| PHP | TypeError: applyCoupon(): Argument #2 ($percent) must be of type int, string given |
form values are always strings under strict_types |
| JS (jsc) | ReferenceError: Can't find variable: totl |
typo; V8 says totl is not defined (not run) |
| JS (jsc) | TypeError: null is not an object (evaluating 'null.length') |
missing value from an API |
| Java | ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3 |
<= instead of < |
| Java | error: cannot find symbol |
compile error: typo or missing import |
| Dart | RangeError (length): Invalid value: Not in inclusive range 0..2: 3 |
off-by-one |
| Dart | Null check operator used on a null value |
a ! on a missing map key |
| Swift | Fatal error: Index out of range |
off-by-one; exit code 133 |
| C | exit code 139, no message | segmentation fault; rebuild with -fsanitize=address -g |
| C | error: call to undeclared function 'totl' |
-Wimplicit-function-declaration; missing #include or typo |
Common mistake: printing only the message
The mistake. You catch the exception and print it:
try:
total = order["items"][0]["price"] * order["items"][0]["qty"]
except Exception as e:
print("error:", e) # what beginners write
print("repr: ", repr(e)) # better: shows the type
traceback.print_exc() # best: the whole story
What actually happens.
error: 'qty'
repr: KeyError('qty')
Traceback (most recent call last):
File "~/demo/swallow.py", line 5, in <module>
total = order["items"][0]["price"] * order["items"][0]["qty"]
KeyError: 'qty'
str(e) of a KeyError is just 'qty'. In a log full of lines like error: 'qty', nobody knows what kind of error it was or where it came from. The fix: log the type and the stack, with traceback.print_exc() or, in real code, logger.exception(...) (lecture 2 shows it). The same goes for every language: Java's e.printStackTrace() or a logger with the exception as last argument, PHP's (string) $e, and JavaScript's e.stack.
A second classic: opening json/decoder.py and trying to fix the standard library. If the newest frame is in code you did not write, go up to the newest frame you did write.
How to search an error message well
Search engines and issue trackers match words. Your error line contains words nobody else has: your home directory, your order id, a memory address, a process id. Strip them. This small filter does it for the examples above:
import re
import sys
RULES = [
(r"(?:[A-Za-z]:)?(?:/[\w.@~-]+)+/", ""), # directories -> keep file name only
(r"0x[0-9a-fA-F]+", ""), # addresses
(r"==\d+==", ""), # sanitizer PIDs
(r"'[^']{12,}'|\"[^\"]{12,}\"", "'…'"), # long quoted user data
(r"\b[A-Z]-\d+\b|\b\d{4,}\b", ""), # ids, big numbers
(r"\s+", " "),
]
def error_key(line):
for pattern, repl in RULES:
line = re.sub(pattern, repl, line)
return line.strip()
for raw in sys.stdin:
if raw.strip():
print(error_key(raw))
$ python3 errkey.py < samples.txt
FileNotFoundError: [Errno 2] No such file or directory: '…'
PHP Fatal error: Uncaught JsonException: Syntax error in orders.php:18
java.lang.IllegalArgumentException: bad qty in order
ERROR: AddressSanitizer: heap-buffer-overflow on address at pc bp sp
Then search in quotes for the stable part, add the language or library name and, if behaviour changed recently, its version: "Unexpectedly found nil while unwrapping an Optional value" swift dictionary. If your own code produced the message (bad qty in order), searching the web is pointless: search your repository for the string instead, which takes you straight to the throw.
Honest limits and version notes
- Python 3.9 is what ships with Xcode's command line tools here. Python 3.11 adds
^^^^markers under the exact sub-expression that failed, and 3.10 adds "Did you mean …?" hints toNameErrorandAttributeError. We did not run either. - Java's detailed NullPointerException messages arrived in JDK 14 (JEP 358) and became the default in JDK 15; we checked them on 26. Our
Caused byblock showed launcher frames because we used single-filejava Orders.java; a compiled app prints... N moreinstead. - JavaScript's
Errorcauseoption is ES2022. Node and browsers print causes differently from jsc; we only ran jsc. - Swift's built-in crash backtracer refused to run in our setup ("backtrace-on-crash is not supported for privileged executables"), so we used lldb. On Linux it is on by default.
- The ASan line counts and addresses change between runs; the frames do not.
Exercises
- A Python traceback ends with
ValueError: invalid literal for int() with base 10: '3 a'. Its last three frames are in~/project/app.py,…/site-packages/pandas/…and…/site-packages/pandas/…. Where do you look first? - A Java log shows
IllegalStateException: checkout failedfollowed by twoCaused by:blocks:SQLExceptionand thenSocketTimeoutException. Which one do you investigate? - Turn this into a good search query:
PHP Warning: Undefined array key "qty" in /var/www/shop/src/orders.php on line 5.
Answers
- The newest frame in
app.py: that is the line that passed'3 a'to pandas. Pandas is reporting your data, not its own bug. - The last one,
SocketTimeoutException: the database call timed out, and everything above it is wrappers. Then check why the network or the database was slow. "Undefined array key" phpand, if it helps, the PHP version. Drop the path, the line number andqty. Better still, read line 5 first: the message already tells you the key is missing.
Next: Lecture 2: print, logging and the debugger · Later in this series: your first unit tests, mocks and fixtures, flaky tests and git bisect. For more on sanitizers and lldb, see C undefined behaviour. Back to the programming learning roadmap.
FAQ
Should I read a stack trace from the top or from the bottom?
It depends on the language. A Python traceback prints the most recent call last, so the line that failed is at the bottom, just above the error type. Java, JavaScript, Dart, PHP, lldb and AddressSanitizer print the most recent call first, so the failing frame is at the top. In both cases, look for the newest frame that is in your own file.
What does 'Caused by' mean in a Java stack trace?
Someone caught an exception and threw a new one with the original attached as its cause. The first block is the wrapper; the Caused by block is the original problem and usually the one to fix. Python prints the same idea as 'The above exception was the direct cause of the following exception', with the original first, and PHP prints the wrapper after the word Next.
How do I search for an error message on the web?
Copy the error type and the fixed part of the message, and remove everything that is specific to your machine: directories, memory addresses, process ids, order ids and long quoted values. 'Uncaught JsonException: Syntax error' finds answers; the same line with your home directory and line number mostly finds nothing.