Error Handling¶
CEL evaluation surfaces failures as Python exceptions. This guide covers the exception types you'll encounter and a canonical pattern for safely evaluating untrusted expressions.
Exception types¶
| Exception | When it's raised |
|---|---|
ValueError |
Parse / compile errors, including malformed syntax and empty expressions. |
RuntimeError |
Undefined variables, undefined functions, custom-function failures. |
TypeError |
Type mismatches — incompatible operands, no matching overload, etc. |
ValueError — parse and compile errors¶
from cel import evaluate
try:
evaluate("1 + + 2")
except ValueError as e:
assert "Failed to parse expression" in str(e)
try:
evaluate("")
except ValueError as e:
assert "Failed to parse expression" in str(e)
Malformed input (unclosed quotes, mixed quote types, invalid syntax) raises ValueError cleanly — the library never panics or crashes the process.
RuntimeError — variable and function errors¶
try:
evaluate("undefined_var", {})
except RuntimeError as e:
assert "Undefined variable or function" in str(e)
try:
evaluate("missing_func()", {})
except RuntimeError as e:
assert "Undefined variable or function" in str(e)
TypeError — incompatible operand types¶
try:
evaluate("1 + 2u") # mixed signed/unsigned int
except TypeError as e:
assert "overload" in str(e).lower() or "signed and unsigned" in str(e)
try:
evaluate('"hello" && true') # non-bool in a logical op
except TypeError as e:
assert "No such overload" in str(e)
CEL has no implicit numeric coercion: int + double, int + uint, and similar combinations all raise TypeError. Use int(x), uint(x), or double(x) to convert explicitly.
Safe evaluation wrapper¶
For untrusted input, wrap evaluation with a single handler that converts all CEL exceptions to a sentinel value:
from cel import evaluate
from typing import Any, Optional, Dict
import logging
log = logging.getLogger(__name__)
def safe_evaluate(
expression: str,
context: Optional[Dict[str, Any]] = None,
) -> Optional[Any]:
"""Evaluate a CEL expression, returning None on any failure."""
try:
return evaluate(expression, context)
except (ValueError, RuntimeError, TypeError) as e:
log.warning("CEL evaluation failed: %s (expr=%r)", e, expression)
return None
# Examples
assert safe_evaluate("user.age >= 18", {"user": {"age": 25}}) is True
assert safe_evaluate("1 + + 2") is None # parse error
assert safe_evaluate("missing", {}) is None # undefined variable
assert safe_evaluate("1 + 'oops'") is None # type error
Defensive expression patterns¶
Within the expression itself, use has() and ternaries to short-circuit around missing fields rather than relying on exception handling:
# Safe field access using has()
expr = '''
has(user.profile) && has(user.profile.email)
? user.profile.email
: "no-email"
'''
result = evaluate(expr, {"user": {"profile": {"email": "alice@example.com"}}})
assert result == "alice@example.com"
# Default values for optional fields
result = evaluate(
'has(config.timeout) ? config.timeout : 30',
{"config": {}},
)
assert result == 30
Pre-compilation for performance¶
If you're evaluating the same expression many times, compile once and reuse the program. Parse errors surface at compile() time; runtime errors at execute() time, which lets you handle the two failure modes separately:
from cel import compile
program = compile("user.age >= 18")
# Then on each call:
try:
allowed = program.execute({"user": {"age": 25}})
except (RuntimeError, TypeError) as e:
log.warning("Policy evaluation failed: %s", e)
allowed = False
assert allowed is True
Testing error scenarios¶
When testing code that evaluates CEL expressions, assert on the exception type — not the exact message, which can drift with cel-rust releases:
import pytest
from cel import evaluate
def test_invalid_syntax():
with pytest.raises(ValueError):
evaluate("1 + + 2")
def test_undefined_variable():
with pytest.raises(RuntimeError):
evaluate("missing_var", {})
def test_type_mismatch():
with pytest.raises(TypeError):
evaluate("1 + 2u")
Best practices¶
- Catch by type, not message. Exception classes are part of the public API; message text is not.
- Use
has()for optional fields rather than catching exceptions from inside an expression. - Pre-compile hot-path expressions with
compile()so parse errors surface once, at startup. - Log the failing expression when you catch an evaluation error — the expression text is usually the most useful debugging info.
- Don't sandbox by exception handling alone if the expression source is untrusted; also limit input size, expression depth, and execution time.