Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Error Handling Operators

Operators for throwing and catching errors inside a rule.

Feature flag (Rust crate). try and throw require the error-handling feature. 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 error
  • fallback - Value or expression to use if an error occurs
  • try tries 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: var and val return null for an absent path, so try does not fall back on missing data. Use ?? or var’s default argument for that (see Control Flow). An engine configured with EvaluationConfig::missing_var set to MissingVar::Error raises VariableNotFound instead, and try catches it like any other error (see Missing Variables).
  • An exhausted operation budget (BudgetExceeded) is final: try passes 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 throw produces 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, under MissingVar::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’s type field, or
  • error_object - An error object value (sourced from data, or built in templating mode) with arbitrary keys such as code and message. 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: type is an operator name, so it runs the type operator on "X" and throws { "type": "string" }. An error object carrying a type key 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