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:

  1. The last line is the error type and message: KeyError: 'qty'. A KeyError means "this dictionary has no such key". The message is the missing key.
  2. The header says most recent call last. So the frame just above the error is where it failed: line_total, line 11.
  3. Walk upwards only if you need to know how you got there: order_total called it from a generator expression (<genexpr>), main called that, and the module level called main.

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.

Flow diagram in two rows. Step 1, read the last line for the error type and message. Step 2, find the newest frame that is in your own file. A decision box asks whether the error is chained, shown by words such as cause, Caused by or Next. If yes, step 3 reads the root cause block. Both paths lead to step 4, inspecting the data at that line, and then step 5, searching the message with paths and ids stripped.
The reading order that works in every language: last line first, then the newest frame in your own file, then the root cause if the error is chained, then the data, and only then a search.

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
Bar chart of frames printed. Java with a Caused by chain: 12 frames, 4 in your file. Swift under lldb bt: 8 frames, 3 yours. Python JSONDecodeError: 7 frames, 3 yours. C with AddressSanitizer, access plus allocation stacks: 7 frames, 4 yours. PHP with a Next chain: 5 frames, all 5 in your file. Dart: 5 frames, 3 yours. JavaScriptCore: 2 frames, both yours.
Stack frames printed by each runtime for this article's crash examples, with how many of them point into the reader's own file (Apple Silicon Mac M1 Pro, one run each; frame counts are deterministic).

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 to NameError and AttributeError. 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 by block showed launcher frames because we used single-file java Orders.java; a compiled app prints ... N more instead.
  • JavaScript's Error cause option 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

  1. 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?
  2. A Java log shows IllegalStateException: checkout failed followed by two Caused by: blocks: SQLException and then SocketTimeoutException. Which one do you investigate?
  3. Turn this into a good search query: PHP Warning: Undefined array key "qty" in /var/www/shop/src/orders.php on line 5.

Answers

  1. 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.
  2. 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.
  3. "Undefined array key" php and, if it helps, the PHP version. Drop the path, the line number and qty. 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.

  • #debugging
  • #stack trace
  • #traceback
  • #error messages
  • #beginner

t.laboratory

A technical blog that records real problems from the apps, servers and services we build and run — and the code that fixed them.

About · Apps

↑