In this video, I talk about variable scopes and explains who you should stop using most global variables.

In this video, Erik revisits a perennial topic in AL development: when to use global variables versus local variables. Prompted by reviewing some code with subtle bugs caused by improper variable scoping, he walks through the historical reasons why Business Central’s codebase is littered with globals, demonstrates the problems they cause, and shows a better approach rooted in functional programming principles.
The Historical Legacy of Global Variables
To understand why global variables are so prevalent in Business Central, we need to go back in time. The old Navision character-based client only had global variables — local variables simply didn’t exist. When the first version of Navision Financials was written, it was created in the character-based environment and then converted. This means the entire foundation was built with globals, and we’ve inherited that legacy even 26 years later.
You can still see the evidence today. If you open the base application and look at codeunits like Codeunit 12 or Codeunit 22, you’ll find a large number of global variables. Take a variable like TempSplitItemJournalLine in Codeunit 22 — it’s used 42 times across multiple procedures. It takes significant effort to figure out the scope and relationships of such variables, and it’s extremely hard to debug when something goes wrong.
The Rule: Do Not Use Global Variables (By Default)
Erik recalls that early on, his team established a guideline: do not use global variables. Of course, there are scenarios where you must use a global variable, but by default you should almost never reach for one.
A Common Anti-Pattern on Pages
One pattern Erik sees frequently involves page development. A developer wants to display a calculated value on a list page, so they create a global variable, populate it in the OnAfterGetRecord trigger, and bind a field to it:
// Anti-pattern: using a global variable
field(test; GlobalText)
{
Caption = 'Global Text';
ApplicationArea = All;
}
trigger OnAfterGetRecord()
begin
GlobalText := Format(Random(100000));
end;
var
GlobalText: Text;
This works, but the variable doesn’t need to be global. The danger is that some other code could introduce a side effect that changes the value of GlobalText unexpectedly.
The Better Approach: Use a Function
Instead of a global variable, replace it with a function call. The field on the page calls the function directly, and no global state is needed:
field(test; GlobalText(Rec))
{
Caption = 'Global Text';
ApplicationArea = all;
}
The function itself is self-contained:
procedure GlobalText(Customer: Record Customer): Text
begin
exit(Customer.Name + ' ' + Format(Random(100000)));
end;
Why Pass Rec as a Parameter?
An important nuance: rather than referencing Rec directly inside the function (which creates a dependency on global state), Erik passes the record as a parameter. This makes the function completely encapsulated. It only uses the parameters given to it and only returns a value. In computer science terms, this is functional programming — no side effects, parameters in, values out.
The benefits are significant:
- Reusability — The function can be exposed and used elsewhere (reports, other pages) by simply passing a customer record.
- Testability — You don’t need to set up an entire environment; just call the function with a parameter and verify the result.
- Predictability — No hidden dependencies on global state mean no surprise behaviors.
The Danger of var (By Reference) Parameters
Erik demonstrates another subtle problem: passing records by reference using the var keyword. Consider this code from the final source:
procedure test2(var Customer: Record Customer)
begin
Customer.Name := 'Erik';
end;
Because the customer record is passed by reference, calling test2 modifies the original record. In the GlobalText function, this means the customer’s name gets changed before it’s used in the return value — every customer on the page shows “Erik” as their name.
This gets even more dangerous in loops. Consider the loop pattern:
procedure test3()
var
sh: Record "Sales Header";
sl: Record "Sales Line";
begin
repeat
test4(sl);
until true;
end;
procedure test4(var sl: Record "Sales Line")
begin
sl."Document Type" := sl."Document Type"::Invoice;
sl.Insert();
if 4 > 6 then
sl.Quantity := 10;
sl.Modify();
end;
Because the sales line is passed with var, any changes made inside test4 persist back into the loop. If a conditional assignment (like setting Quantity := 10) fires on the first iteration but not the second, the second line still gets the quantity of 10 because the value carried over from the previous iteration. This creates what Erik calls a “pseudo global” — a variable that behaves like a global because its scope has been inadvertently expanded through by-reference passing.
Scoping: Use the Minimum Scope Needed
In other programming languages, there’s extensive discussion about scope — the context in which a variable lives. Some languages allow multiple scopes within a single procedure (using blocks or closures). AL, inheriting from Pascal, essentially has three levels:
- Globals — visible across the entire object
- Locals — visible within a single procedure
- Var (by-reference) transfers — which can effectively span multiple scopes
The rule is simple: never assign a variable a scope larger than what you need. If a variable is only used inside one procedure, it should be local to that procedure. If a record is only needed inside a loop body, don’t declare it at a higher level where it can leak state.
When Global Variables Are Actually Needed
There are legitimate cases for global variables:
- Report datasets — A common scenario where you need globals to maintain state across data items.
- Preserving state across triggers — When you need to keep something in memory from one trigger to another (e.g.,
OnAfterGetRecordto an action trigger). - Single-instance codeunits — When a codeunit subscribes to events and needs to preserve state that isn’t part of the event signature. Erik gives the example of his PcScript compiler project, where a single-instance codeunit holds query string parameters across event calls. Since Business Central is not multi-threaded per session, this works well.
- Cross-event state — Passing information from one event subscriber to another where no direct parameter passing is possible.
But in general, Erik’s advice is: you should feel just a tiny bit dirty every time you create a global variable, because you probably don’t need it.
The Complete Source Code
Here’s the full example page demonstrating these patterns:
page 50108 "Global vs Local"
{
Caption = 'Globals vs locals';
PageType = List;
SourceTable = Customer;
layout
{
area(Content)
{
repeater(Rep)
{
field("No."; Rec."No.")
{
ApplicationArea = All;
}
field(Name; Rec.Name)
{
ApplicationArea = All;
}
field(Address; Rec.Address)
{
ApplicationArea = All;
}
field(test; GlobalText(Rec))
{
Caption = 'Global Text';
ApplicationArea = all;
}
}
}
}
procedure GlobalText(Customer: Record Customer): Text
begin
test2(Customer);
exit(Customer.Name + ' ' + Format(Random(100000)));
end;
procedure test2(var Customer: Record Customer)
begin
Customer.Name := 'Erik';
end;
procedure test3()
var
sh: Record "Sales Header";
sl: Record "Sales Line";
begin
repeat
test4(sl);
until true;
end;
procedure test4(var sl: Record "Sales Line")
begin
sl."Document Type" := sl."Document Type"::Invoice;
sl.Insert();
if 4 > 6 then
sl.Quantity := 10;
sl.Modify();
end;
}
Debunking the Performance Myth
A common urban myth Erik addresses: “If I create a lot of local variables, my code will be slower.” This is simply not the case. The amount of memory your computer has is vast, and whether a function call needs to allocate 10 bytes or 2 kilobytes on the stack is irrelevant for the kind of work we do in AL. Unless you’re writing a ray tracer or calling a procedure billions of times in a tight inner loop (which you wouldn’t do in AL anyway), the number of local variables has no meaningful performance impact. Focus on writing readable, understandable code instead.
Summary
The key takeaways from this video are:
- Default to local variables. Only use globals when you have a specific, justified reason.
- Prefer functions over global state. Instead of populating a global variable in a trigger, use a function that returns a value.
- Pass dependencies as parameters. Don’t rely on implicit global state inside your functions — make dependencies explicit through parameters to achieve true encapsulation.
- Be careful with
varparameters. By-reference passing can create pseudo-globals that carry state across loop iterations and produce subtle, hard-to-find bugs. - Use the minimum scope necessary. A variable should never have a broader scope than what it actually needs.
- Don’t worry about performance. Local variables don’t slow your code down — but global variables do make your code harder to understand, debug, and maintain.