Morten Laske AI × Business Central
← Writing

AL's `and` / `or` are eager — your guard clause does not protect you

AL's `and` / `or` are eager — your guard clause does not protect you

Every short-circuit reflex you brought from C# or JavaScript is a latent bug in AL. Here is one that looks impossible until you know the rule. You write a careful guard:

if (StrLen(Code) <= 20) and Rec.Get(Code) then
    Message('Found: %1', Rec.Description);

You are protecting Rec.Get from a too-long key. And yet, on a 25-character input, you get a runtime error from Get about the value being too long — the exact error your StrLen check exists to prevent. The guard did nothing.

The rule: AL boolean operators are not short-circuiting

In C#, JavaScript, most languages you have used, a && b evaluates b only if a is true. That is short-circuit evaluation, and you lean on it constantly for guards.

AL’s and and or evaluate both operands, always. There is no short-circuit. So in the expression above, Rec.Get(Code) runs regardless of the StrLen result. The boolean and only decides what to do with the two results after both have already executed — and by then Get has already thrown.

This is not a compiler bug or a deprecation. It is how the language has always worked, and it will quietly outlive every refactor you do on top of it.

C# / JS — short-circuit
StrLen ≤ 20= false
&& stop
Rec.Get(x)skipped
guard holds — Get never runs
AL — eager
StrLen ≤ 20= false
and ↓ both
Rec.Get(x)RUNS → throws
guard does nothing — exception fires
Same expression, two evaluation models. In AL the right operand runs even when the left already decided the result — so the dangerous call fires before the and ever combines anything.

Why it works this way

It helps to stop thinking of and/or as control flow and start thinking of them as ordinary operators, like +. When you write a + b, you do not expect b to be skipped depending on a — both operands are evaluated, then the operator combines them. AL’s boolean operators are exactly that: functions of two fully-evaluated arguments. The boolean result only governs the then/else branch after both sides have already produced their values (and their side effects, and their exceptions).

Short-circuit languages bolt special evaluation rules onto && and || precisely so they can double as guards. AL never made that bargain. Nothing is wrong; it simply is not the tool you reached for out of habit.

The fix: nest the conditions, or exit early

Make the dependency explicit. Either nest:

if StrLen(Code) <= 20 then
    if Rec.Get(Code) then
        Message('Found: %1', Rec.Description);

or, usually cleaner, guard with an early exit so the dangerous call is unreachable when the precondition fails:

if StrLen(Code) > 20 then
    exit;
if Rec.Get(Code) then
    Message('Found: %1', Rec.Description);

Both express the real intent: do not even attempt Get unless the key is safe.

Where this actually bites

The trivial example is easy to spot. The ones that cost real time are the ones where the second operand is expensive or side-effecting and the first operand is the thing meant to gate it:

  • if IsAllowed(User) and DeleteEverything() — both run; DeleteEverything is not gated.
  • if Rec.FindSet() and (Rec.Count > 1000) — fine, but flip the operands and a method with side effects runs on an empty set.
  • Any guard of the form if HasValue and ParseValue() where ParseValue throws on the empty case you were trying to exclude.

The pattern to train your eyes on: a boolean and/or where the left operand is a safety check and the right operand is the thing being made safe. In AL, that is not a guard. It is two statements with a misleading shape.

or has the mirror-image trap

Everything above applies to or, flipped. With short-circuit ||, the right side runs only if the left is false — so people use it as an “unless we already know it’s fine, check this” guard:

// Intended: if we already have it, don't bother parsing
if Found or TryParse(Raw) then ...;

In AL, TryParse(Raw) runs even when Found is already true. If TryParse has a side effect — it logs, it increments a counter, it mutates a global — that side effect fires on every call, including the ones you thought Found would skip. The bug is silent because the boolean result is still correct; only the side effect is wrong, and side-effect bugs are the ones that take a week to trace back to a one-line or.

A worse version, with a side effect

The trivial StrLen/Get case throws loudly, which at least gets your attention. The expensive version is the one that does damage quietly:

// Looks gated. Isn't. Posting runs no matter what IsApproved returns.
if IsApproved(Doc) and PostDocument(Doc) then
    Message('Posted.');

PostDocument executes on unapproved documents too, because AL evaluated it before the and combined anything. There is no exception here — just a document that got posted when it should not have. You will find this one in production, not in the debugger.

The mental model

Treat AL and/or as “evaluate both, then combine,” never as “evaluate left, maybe evaluate right.” If correctness depends on one side not running, a boolean operator is the wrong tool — reach for nesting or an early exit. Short-circuit logic in AL lives in control flow, not in expressions.

Once that clicks, a whole category of “but I checked for that” bugs stops being mysterious.

Found this useful? Share on LinkedIn · email me a correction or follow-up.

Related