Skip to content

Common errors

CEL expressions can fail at runtime if they encounter operations that have no valid result, such as dividing by zero or accessing a list element that doesn't exist. In Protovalidate, a runtime error in a CEL expression causes the validation rule to fail with an error rather than returning a clean pass or fail result. This page covers the most common CEL runtime errors and the guard patterns you can use to avoid them.

Integer division by zero produces a runtime error in CEL. Unlike floating-point division, which returns infinity, integer division has no valid result for a zero divisor.

Error: division by zero
this.total / this.count

If count is 0, this expression fails at runtime. The fix is to guard the division with a short-circuit check. Because && in CEL is short-circuit evaluated, the right side is only evaluated when the left side is true:

Guard with short-circuit evaluation
this.count != 0u && this.total / this.count >= 10u

In a Protovalidate rule, this pattern looks like:

Guarded division in a Protovalidate rule
import "buf/validate/validate.proto";
message Stats {
// Total number of events observed.
uint64 total = 1;
// Number of periods over which events were counted.
uint64 count = 2;
option (buf.validate.message).cel = {
id: "stats.average_minimum"
message: "average must be at least 10"
// Guard against division by zero using short-circuit evaluation.
expression: "this.count != 0u && this.total / this.count >= 10u"
};
}

Accessing a list element by index fails at runtime if the index is beyond the list's size. This includes accessing index 0 on an empty list.

Error: index out of bounds
this.tags[0] == "primary"

If tags is empty, this expression fails. Guard it by checking the size first:

Guard with size check
size(this.tags) > 0 && this.tags[0] == "primary"

Accessing a map key that doesn't exist produces a runtime error. This applies to Protobuf map fields as well as any CEL map value.

Error: missing map key
this.labels["env"] == "production"

If the labels map has no "env" key, this expression fails. Guard it with the in operator, which checks for key existence without accessing the value:

Guard with the in operator
"env" in this.labels && this.labels["env"] == "production"

In a Protovalidate rule:

Guarded map access in a Protovalidate rule
import "buf/validate/validate.proto";
message Deployment {
// Key-value labels describing the deployment.
map<string, string> labels = 1 [(buf.validate.field).cel = {
id: "deployment.labels.env"
message: "deployments must be labeled with an env of 'production' or 'staging'"
// Guard with `in` before accessing the key.
expression: "'env' in this && (this['env'] == 'production' || this['env'] == 'staging')"
}];
}

Protobuf wrapper types like google.protobuf.StringValue, google.protobuf.Int32Value, and others evaluate to null in CEL when the field is unset. Using a null value in an operation that expects a concrete type produces a runtime error.

Error: null wrapper value
this.display_name.size() > 0

If display_name is an unset google.protobuf.StringValue, it evaluates to null and calling size() on it fails. Guard it with the has() macro, which checks whether a field is set without evaluating its value:

Guard with has()
has(this.display_name) && this.display_name.size() > 0

In a Protovalidate rule:

Guarded wrapper access in a Protovalidate rule
import "buf/validate/validate.proto";
import "google/protobuf/wrappers.proto";
message Profile {
// Optional display name for the user.
google.protobuf.StringValue display_name = 1;
option (buf.validate.message).cel = {
id: "profile.display_name_length"
message: "display name, if provided, must be at least 3 characters"
// Guard against null wrapper with has().
expression: "!has(this.display_name) || this.display_name.size() >= 3"
};
}

Notice the inverted pattern here: !has(this.display_name) || ... means "either the field isn't set (which is fine) or, if it is set, it must pass this check." This is a common way to express optional-but-validated fields.

When your expression needs to produce a value rather than just return true or false, the ternary operator provides a way to supply a safe default. This is especially useful when the expression is part of a larger computation.

Safe field access with a default
has(this.nickname) ? this.nickname : this.full_name

This pairs well with the division-by-zero guard when you need the result of the division itself:

Safe division with a default
this.count != 0u ? this.total / this.count : 0u