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.
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;DeleteEverythingis 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()whereParseValuethrows 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.