Skip to main content

Updated Aug 7, 2026

Error handling and exceptions

Every failure in this library — a malformed parse, a wrong-type access, a missing key, an invalid iterator — surfaces as one of five exception types, and every one of them carries a stable numeric id you can match on. Learning this hierarchy once pays off across the whole library.

The hierarchy​

All five derive from json::exception, which derives from std::exception, so a single catch (const json::exception& e) catches everything the library can throw, while e.what() and e.id let you distinguish cases when you need to.

What throws what​

Exceptionid rangeTypical trigger
parse_error1xxparse_error.101 — unexpected token while parsing malformed JSON text
invalid_iterator2xxinvalid_iterator.202 — dereferencing or advancing an iterator that no longer points into the container
type_error3xxtype_error.302 — get<T>() called with a T the stored value can't convert to; type_error.305 — operator[] called with a key on a json that's an array or scalar, not an object
out_of_range4xxout_of_range.403 — key not found in .at()
other_error5xxMiscellaneous — e.g. a failing test operation in patch()

Reading the message​

e.what() always begins with a bracketed identifier of the exact form [json.exception.type_error.302], followed by a human-readable description — the bracketed part is what you'd grep for in logs, the rest is for a person reading them. For parse_error specifically, e.byte gives the byte offset into the input where the parser gave up, which is usually enough to locate the bad token without re-running the parse with extra instrumentation.

try {
json j = json::parse(bad_input);
} catch (const json::parse_error& e) {
std::cerr << "id=" << e.id << " byte=" << e.byte << " " << e.what() << "\n";
}

Parsing without exceptions​

If your codebase avoids exceptions on the parse path specifically, the allow_exceptions = false overload returns a discarded value instead of throwing:

json j = json::parse(input, /* cb */ nullptr, /* allow_exceptions */ false);
if (j.is_discarded()) {
// handle failure without a try/catch
return std::nullopt;
}

This only covers the parse call itself — every other operation in the library (access, conversion, patching) still throws on failure regardless of this flag.

Building with exceptions disabled​

For codebases compiled with exceptions turned off entirely, defining JSON_NOEXCEPTION before including the header changes every throw in the library into a call to abort() instead.

With JSON_NOEXCEPTION, malformed input aborts the process

This isn't a graceful degradation — it's a hard process termination the instant any exceptional path is hit, including ordinary malformed input on json::parse. If you build with JSON_NOEXCEPTION, every code path that could reach the library with untrusted or unvalidated data needs to validate it before the call, since the library itself no longer has a way to report the failure back to you.

Diagnostics​

Defining JSON_DIAGNOSTICS=1 before including the header adds a JSON Pointer to the offending element in every exception message — instead of just "wrong type," you get exactly which path in a large document triggered the error, which is often the difference between a five-second fix and a debugging session.

Turn on JSON_DIAGNOSTICS in debug builds

The extra diagnostic information isn't free — it adds a modest amount of runtime state tracking — so it's a reasonable default for debug/development builds and worth turning off for a release build where the extra cost isn't worth paying and the input is already well-tested.

See also​