Python JSON error handling: every JSONDecodeError message (tested)

Python's json module raises JSONDecodeError with about a dozen different messages, accepts some invalid JSON without complaint, and uses plain TypeError when writing. We measured each case in Python 3.13 (and 3.11 where they differ) and wrote a parse helper that shows the text around the error.

1. Every common JSONDecodeError message and what causes it

json.JSONDecodeError is a subclass of ValueError, so except ValueError catches it too. We fed json.loads() the ten broken inputs that come up most and printed e.msg with the position:

import json
print("JSONDecodeError ->", " > ".join(c.__name__ for c in json.JSONDecodeError.__mro__[1:-2]))
cases = [
    ("empty string",          ''),
    ("HTML error page",       '<html>502 Bad Gateway</html>'),
    ("single quotes",         "{'a': 1}"),
    ("trailing comma",        '{"a": 1,}'),
    ("missing comma",         '{"a": 1 "b": 2}'),
    ("unterminated string",   '{"a": "hi}'),
    ("raw newline in string", '{"a": "line1\nline2"}'),
    ("two documents",         '{"a": 1}{"b": 2}'),
    ("UTF-8 BOM",             '{"a": 1}'),
    ("Python True/None",      '{"a": True, "b": None}'),
]
for label, text in cases:
    try:
        json.loads(text); print(f"{label:22} OK")
    except json.JSONDecodeError as e:
        print(f"{label:22} {e.msg} (line {e.lineno} col {e.colno})")

Output:

JSONDecodeError -> ValueError > Exception
empty string           Expecting value (line 1 col 1)
HTML error page        Expecting value (line 1 col 1)
single quotes          Expecting property name enclosed in double quotes (line 1 col 2)
trailing comma         Illegal trailing comma before end of object (line 1 col 8)
missing comma          Expecting ',' delimiter (line 1 col 9)
unterminated string    Unterminated string starting at (line 1 col 7)
raw newline in string  Invalid control character at (line 1 col 13)
two documents          Extra data (line 1 col 9)
UTF-8 BOM              Unexpected UTF-8 BOM (decode using utf-8-sig) (line 1 col 1)
Python True/None       Expecting value (line 1 col 7)

Version note: the trailing-comma message is new in Python 3.13. On Python 3.11 (we ran the same file in python:3.11-alpine) the same input gives Expecting property name enclosed in double quotes (line 1 col 9), the same message as single quotes. So an older Python reporting "double quotes" may really mean a trailing comma.

Deep dives for each message: Expecting value: line 1 column 1 (char 0) (empty body, HTML error page, Python literals), Expecting property name enclosed in double quotes, Invalid control character, Extra data. For the BOM, open the file with encoding="utf-8-sig" as the message says.

2. What json accepts without any error

Some invalid JSON does not raise, which matters if other systems will read your data:

import json, math
print(json.loads('{"a": NaN, "b": Infinity}'))           # accepted by default
print(json.dumps({"x": math.nan}))                         # written by default

def strict_const(name):
    raise ValueError(f"non-standard JSON constant {name}")
try:
    json.loads('{"a": NaN}', parse_constant=strict_const)
except ValueError as e:
    print("parse_constant:", e)
try:
    json.dumps({"x": math.nan}, allow_nan=False)
except ValueError as e:
    print("allow_nan=False:", e)
print(json.loads('{"a": 1, "a": 2}'))                     # duplicate key: last wins

Output:

{'a': nan, 'b': inf}
{"x": NaN}
parse_constant: non-standard JSON constant NaN
allow_nan=False: Out of range float values are not JSON compliant: nan
{'a': 2}

NaN and Infinity are not valid JSON, but Python reads and writes them by default, and a JavaScript JSON.parse on the other end will fail. Use allow_nan=False when writing and parse_constant when reading if you need strict JSON. Duplicate keys are silently resolved to the last value.

3. A parse helper that says where the JSON broke

The exception has msg, doc, pos, lineno and colno, so you can show the text around the error instead of only "line 1 column 26":

import json

def parse(text, source="input"):
    try:
        return json.loads(text)
    except json.JSONDecodeError as e:
        start = max(e.pos - 20, 0)
        snippet = e.doc[start:e.pos + 20]
        raise ValueError(f"{source}: {e.msg} at line {e.lineno} col {e.colno}: "
                         f"...{snippet!r}...") from None

for text in ['{"user": "ann", "age": 31,}', '<html>502 Bad Gateway</html>']:
    try:
        parse(text, "api response")
    except ValueError as e:
        print(e)

data = parse('{"user": "ann"}')
print("missing key with .get():", data.get("age"))
try:
    data["age"]
except KeyError as e:
    print("missing key with []:", type(e).__name__, e)

Output:

api response: Illegal trailing comma before end of object at line 1 col 26: ...'r": "ann", "age": 31,}'...
api response: Expecting value at line 1 col 1: ...'<html>502 Bad Gatewa'...
missing key with .get(): None
missing key with []: KeyError 'age'

The snippet makes the second case obvious: the API returned an HTML error page, not JSON. Catch JSONDecodeError only around the parse call, and handle missing keys separately: a KeyError after a successful parse is a different problem (the JSON is valid, it just doesn't have that field). Use .get() for optional fields.

4. The encoding side: TypeError is not JSON serializable

json.dumps() raises TypeError, not a JSON-specific exception, for any type it doesn't know:

import json, datetime, decimal
for value in [{1, 2}, datetime.date(2026, 10, 22), decimal.Decimal("1.10"), b"raw"]:
    try:
        json.dumps(value)
    except TypeError as e:
        print(f"{type(value).__name__:8} TypeError: {e}")

def default(o):
    if isinstance(o, (datetime.date, datetime.datetime)): return o.isoformat()
    if isinstance(o, decimal.Decimal): return str(o)
    if isinstance(o, set): return sorted(o)
    raise TypeError(f"can't serialize {type(o).__name__}")
print(json.dumps({"d": datetime.date(2026, 10, 22), "p": decimal.Decimal("1.10"), "s": {2, 1}}, default=default))

Output:

set      TypeError: Object of type set is not JSON serializable
date     TypeError: Object of type date is not JSON serializable
Decimal  TypeError: Object of type Decimal is not JSON serializable
bytes    TypeError: Object of type bytes is not JSON serializable
{"d": "2026-10-22", "p": "1.10", "s": [1, 2]}

A default= function converts the types you know and must raise TypeError for the rest, so real bugs still surface. More in our Object of type X is not JSON serializable guide and the numpy int64 version.

Frequently asked questions

How do I handle JSON errors in Python?

Wrap json.loads/json.load in try/except json.JSONDecodeError (a ValueError subclass) and use e.msg, e.lineno, e.colno and e.doc[e.pos-20:e.pos+20] to report where it broke. json.dumps raises TypeError for unsupported types.

Is JSONDecodeError a ValueError?

Yes. json.JSONDecodeError subclasses ValueError, so except ValueError also catches it.

Why does Python json accept NaN?

json.loads and json.dumps allow NaN, Infinity and -Infinity by default even though they are not valid JSON. Pass allow_nan=False to dumps, and a parse_constant function to loads to reject them.

What does Illegal trailing comma before end of object mean?

A comma after the last item, like {"a": 1,}. Python 3.13 added this message; Python 3.11 reports the same input as Expecting property name enclosed in double quotes.

Written with AI assistance. Measured on 2026-10-22 on Debian with Python 3.13.5 (json module), with the decode-message table also run on Python 3.11.17 (python:3.11-alpine) to compare the trailing-comma message. Every block was run verbatim and its output pasted unedited. PlainHeaders is our extension; Python is free.