Error Handling Operators
Operators for throwing and catching errors inside a rule.
Feature flag (Rust crate).
tryandthrowrequire theerror-handlingfeature. Every language binding enables it. See the feature table.
Argument counts: try takes one or more arguments ({ "try": [] } is null, and a single argument evaluates as itself with nothing caught). throw reads one argument; with none it throws { "type": "null" }.
try
Catch errors and provide fallback values.
Syntax:
{ "try": [expression, fallback] }
{ "try": [expression, catch_expression] }
{ "try": [expression, fallback1, fallback2, ...] }
Arguments:
expression- Expression that might throw an errorfallback- Value or expression to use if an error occurstrytries further arms in order: each arm runs only if every arm before it raised an error. Only the last arm evaluates with the error object as its context; intermediate arms see the ordinary data context
Returns: Result of expression if successful, or fallback value/expression result if an error occurs.
What counts as an error:
- Anything raised by
throw. - Errors raised by the engine itself: an unknown operator, invalid arguments
(for example
{ "max": ["a", 1] }), an integer division by zero, a datetime that fails to parse, an unknown timezone, and so on. - A missing variable is not an error under the default configuration:
varandvalreturnnullfor an absent path, sotrydoes not fall back on missing data. Use??orvar’s default argument for that (see Control Flow). An engine configured withEvaluationConfig::missing_varset toMissingVar::ErrorraisesVariableNotFoundinstead, andtrycatches it like any other error (see Missing Variables). - An exhausted operation budget
(
BudgetExceeded) is final:trypasses it on without running a fallback arm.
Context in Catch:
When try catches an error, the catch expression evaluates with the error
object as its context, so you read its fields via var / val:
- A string
throwproduces the error object{ "type": <string> }, so you read the message with{ "var": "type" }. - An object
throw(sourced from data) preserves its own keys, so fields such as{ "var": "code" }or{ "var": "message" }read those keys. - Any other thrown value becomes
{ "type": <its type name> }:{ "throw": 5 }gives{ "type": "number" }. - Engine-raised errors arrive as
{ "type": <message> }: an unknown operator gives{ "type": "Unknown Operator" }, bad operands give{ "type": "Invalid Arguments" }, an integer division by zero gives{ "type": "NaN" }, and other kinds carry their message text (for example{ "type": "Invalid datetime format" },{ "type": "Unknown timezone: Mars/Olympus" }, or, underMissingVar::Error,{ "type": "Variable not found: user.name" }). { "var": "" }returns the entire error object.
Examples:
// Simple fallback value
{ "try": [
{ "/": [10, 0] },
0
]}
// Result: 0 (division by zero caught)
// Expression that succeeds
{ "try": [
{ "+": [1, 2] },
0
]}
// Result: 3 (no error, normal result)
// Catch a string error: the string becomes the error object's "type" field
{ "try": [
{ "throw": "User not found" },
{ "cat": ["Error: ", { "var": "type" }] }
]}
// Result: "Error: User not found"
// Canonical pattern: read a thrown string back via "type"
{ "try": [
{ "throw": "Some error" },
{ "val": "type" }
]}
// Result: "Some error"
// Throw an object sourced from data, then read its fields by key
{ "try": [
{ "throw": { "var": "err" } },
{ "var": "code" }
]}
// Data: { "err": { "code": 404, "message": "User not found" } }
// Result: 404
// Engine errors are caught too: read the kind via "type"
{ "try": [{ "not_an_operator": [1] }, { "var": "type" }] }
// Result: "Unknown Operator"
{ "try": [{ "max": ["a", 1] }, { "var": "type" }] }
// Result: "Invalid Arguments"
// Multiple fallback arms: each runs only if the previous one raised
{ "try": [{ "throw": "a" }, { "throw": "b" }, "c"] }
// Result: "c"
// A missing variable is NOT an error, so try does not fall back
{ "try": [{ "var": "missing" }, "fallback"] }
// Data: {}
// Result: null
// Nested try: the inner arm re-throws a clearer message, the outer arm reads it
{ "try": [
{ "try": [
{ "/": [1, { "var": "d" }] },
{ "throw": "division failed" }
]},
{ "var": "type" }
]}
// Data: { "d": 0 }
// Result: "division failed"
// Data: { "d": 4 }
// Result: 0.25
Common Patterns
Safe division:
{ "if": [
{ "!": { "var": "denominator" } },
0,
{ "/": [{ "var": "numerator" }, { "var": "denominator" }] }
]}
// Data: { "numerator": 1 }
// Result: 0
// Data: { "numerator": 1, "denominator": 4 }
// Result: 0.25
try alone is not enough here. Only an integral-valued zero divisor (or a
non-numeric string) raises the NaN error that try can catch. A null,
false, "", or "0" divisor coerces to zero and follows
EvaluationConfig::division_by_zero, whose default (ReturnSaturated)
returns f64::MAX without raising, so
{ "try": [{ "/": [{ "var": "numerator" }, { "var": "denominator" }] }, 0] }
yields 1.7976931348623157e308 when denominator is missing. Configure
DivisionByZeroHandling::ThrowError if you want every zero divisor to reach
the try fallback; see Arithmetic for the full rule.
Default for a missing value (not a try job):
{ "??": [{ "var": "user.profile.settings.theme" }, "default-theme"] }
// Data: {}
// Result: "default-theme"
Under the default configuration var on a missing path returns null
rather than raising, so wrapping it in try never produces the fallback. Use ?? or the var default form,
{ "var": ["user.profile.settings.theme", "default-theme"] }.
Error logging pattern:
{ "try": [
{ "risky_operation": [] },
{ "cat": ["Operation failed: ", { "var": "type" }] }
]}
// Result: "Operation failed: Unknown Operator"
// risky_operation stands in for a custom operator. In the default engine the
// name is not registered, so the call itself raises "Unknown Operator" and is
// caught. For a string throw, the thrown text is in the "type" field; if the
// operation throws a structured object instead, read the relevant key (e.g.
// "message").
Try it:
throw
Throw an error with optional details.
Syntax:
{ "throw": message }
{ "throw": error_object }
Arguments:
message- Error message string. The string becomes the error object’stypefield, orerror_object- An error object value (sourced from data, or built in templating mode) with arbitrary keys such ascodeandmessage. A multi-key object written inline as a literal does NOT compile in the default engine, because the engine parses it as an operator map. A single-key literal such as{ "throw": { "type": "X" } }does not work either, even in templating mode:typeis an operator name, so it runs thetypeoperator on"X"and throws{ "type": "string" }. An error object carrying atypekey must come from data ({ "throw": { "var": "err" } }).
Returns: Never returns normally. The error ends the evaluation unless a try catches it.
Examples:
// Simple string error (the string lands in the error object's "type" field)
{ "throw": "Something went wrong" }
// Throws the error object { "type": "Something went wrong" }
// Error object sourced from data. A literal multi-key object written inline
// would be parsed as an operator map and fail to compile in the default engine.
{ "throw": { "var": "err" } }
// Data: { "err": { "code": "INVALID_INPUT", "message": "Age must be positive" } }
// Throws an error carrying the object's fields
// Richer error: build the object in your data (or enable templating mode) and
// throw it by reference.
{ "throw": { "var": "validationError" } }
// Data: {
// "validationError": {
// "code": "VALIDATION_ERROR",
// "message": "Invalid email format",
// "field": "email"
// }
// }
// Conditional throw (string form)
{ "if": [
{ "<": [{ "var": "age" }, 0] },
{ "throw": "Age cannot be negative" },
{ "var": "age" }
]}
// Data: { "age": -5 }
// Throws the error object { "type": "Age cannot be negative" }
// Data: { "age": 25 }
// Result: 25
Uncaught errors
An error that no try catches reaches your code as an Error. A throw
arrives with ErrorCode::Thrown (tag "Thrown") and the error object as
its payload (Error::thrown_value()). Error::operator() names the
innermost operator that failed (since 5.8.0), here throw, on every
evaluation path, traced or not. The serialized form of the error from
{ "+": [1, { "throw": "bad" }] }:
{"type": "Thrown", "message": "Thrown: {\"type\":\"bad\"}", "thrown": {"type": "bad"}, "operator": "throw", "node_ids": [3, 4]}
See Error in the API reference for the other fields.
Common Patterns
Validation with throw:
{ "if": [
{ "missing": ["name", "email"] },
{ "throw": "Required fields missing" },
"valid"
]}
Business rule enforcement:
{ "if": [
{ ">": [{ "var": "amount" }, { "var": "balance" }] },
{ "throw": "Amount exceeds balance" },
{ "-": [{ "var": "balance" }, { "var": "amount" }] }
]}
Type validation:
{ "if": [
{ "!==": [{ "type": { "var": "value" } }, "number"] },
{ "throw": "Expected number" },
{ "*": [{ "var": "value" }, 2] }
]}
Try it:
Error Handling Patterns
Graceful Degradation
Fallback chains over possibly-missing data belong to ??, not try, because
a missing var is null rather than an error:
{ "??": [
{ "var": "user.preferences.language" },
{ "var": "defaults.language" },
"en"
]}
// Data: { "defaults": { "language": "fr" } }
// Result: "fr"
// Data: {}
// Result: "en"
Reserve try for expressions that can raise: throw, an integer
division by zero, invalid arguments, an unknown operator, a datetime parse
failure.
Validation Pipeline
{ "try": [
{ "if": [
{ "!": { "var": "input" } },
{ "throw": "Input required" },
{ "if": [
{ "<": [{ "length": { "var": "input" } }, 3] },
{ "throw": "Minimum 3 characters" },
{ "var": "input" }
]}
]},
{ "cat": ["Validation error: ", { "var": "type" }] }
]}
// Data: {}
// Result: "Validation error: Input required"
// Data: { "input": "ab" }
// Result: "Validation error: Minimum 3 characters"
// Data: { "input": "abc" }
// Result: "abc"
Error Recovery with Fallback Operations
The variadic form tries each arm in turn:
{ "try": [
{ "primary_operation": [] },
{ "fallback_operation": [] },
"all operations failed"
]}
// Result: "all operations failed"
// primary_operation and fallback_operation stand in for custom operators. Each
// arm runs only if the previous one raised; in the default engine neither name
// is registered, so both raise "Unknown Operator" and the last arm is returned.
Collecting All Errors
JSONLogic has no native way to collect multiple errors, but you can structure validations to report every failure:
{ "filter": [
[
{ "if": [{ "missing": ["name"] }, "name is required", null] },
{ "if": [{ "missing": ["email"] }, "email is required", null] },
{ "if": [
{ "and": [
{ "!": { "missing": ["email"] } },
{ "!": { "in": ["@", { "var": "email" }] } }
]},
"invalid email format",
null
]}
],
{ "!==": [{ "var": "" }, null] }
]}
// Data: { "email": "foo" }
// Result: ["name is required", "invalid email format"]
This returns an array of error messages for all validation failures. To wrap
it in an object such as { "errors": [...] }, enable templating mode
(Engine::builder().with_templating(true), Cargo feature templating). The
default engine parses a top-level errors key as an operator call, which fails
with InvalidOperator.
See Also
- Control Flow Operators — use
??andiffor null-coalescing and branching without throwing - Missing Value Operators — check for required fields before throwing validation errors
- Configuration — configure
NanHandling,DivisionByZeroHandling, andMissingVar - Structured Objects (Templating) — build structured error payloads in rules
- Operators Overview — full operator catalog and feature-flag table