To var, or not to var, that is the question in AL

In this video, I take a look at how to use the var prefix for passing parameters to procedures. I look at common mistakes and some gotchas that you might not be aware of.

https://youtu.be/MveP40FUG1g

In this video, Erik dives into one of the most common sources of subtle bugs in AL development for Business Central: the var keyword on procedure parameters. He explains what “by reference” versus “by value” actually means, demonstrates the classic record-modification error that catches many developers, and reveals a surprising behavior with .NET-backed types like JsonObject that contradicts the official documentation.

The Basics: What Does var Mean?

When you call a procedure in AL and pass parameters, you have a choice: pass them by value (the default) or by reference (using the var keyword). But what does that actually mean?

Going back to foundational computer science — languages like C, C++, and Pascal — the concept revolves around pointers. A pointer is simply a variable that stores an address in memory. When you pass a parameter by reference, instead of copying the value onto the call stack, you’re passing the address of where that value lives in memory. This means the called procedure is manipulating the exact same data as the caller.

A Simple Example: Integers

Consider a procedure that simply increments an integer by one:

procedure ProcA(i: Integer)
begin
    i += 1;
end;

If we call this without var:

var
    i: Integer;
begin
    i := 10;
    ProcA(i);
    Message('%1', i); // Displays 10
end;

The result is 10 — not 11. That’s because when AL calls ProcA, it creates a copy of i and places it on the stack. The increment happens to the copy. When the procedure returns, the copy is discarded, and the original i remains unchanged.

Now, if we add var to the parameter:

procedure ProcA(var i: Integer)
begin
    i += 1;
end;

Now the result is 11. We’re no longer passing a copy — we’re passing the address of i. Both the caller and the procedure are working with the same variable in memory.

You Can’t Pass Constants by Reference

One important detail: if a parameter is marked as var, you cannot pass a constant value like 20 directly. A constant doesn’t have an address in memory — it doesn’t exist on the stack in the same way. You must pass an actual variable. Without var, passing a constant like ProcA(20) is perfectly fine.

The Classic Mistake: Records Without var

This is where things get real. One of the most common mistakes in the entire world of NAV/Dynamics/Business Central involves passing record variables to procedures without var.

Consider this scenario:

procedure ProcA(Vendor: Record Vendor)
begin
    Vendor.Address := 'Hey';
    Vendor.Modify();
end;

And the calling code:

var
    Vendor: Record Vendor;
begin
    Vendor.FindFirst();
    ProcA(Vendor);
    Vendor.Address := 'America';
    Vendor.Modify();
end;

What happens? You get the classic error: “The changes to the Vendor record cannot be saved because some information on the page is not up to date.” Or the variant of this error that says “This vendor has been modified by another user after it was retrieved from the database.”

Erik notes that Microsoft has struggled with the wording of this error — the message keeps changing, but the underlying problem is always the same.

Why Does This Happen?

When ProcA is called without var, a copy of the vendor record is created. At this point, there are two copies of the vendor in memory. The copy inside ProcA gets modified and saved to the database (with address “Hey”). When the procedure returns, the copy is discarded.

Back in the calling code, we still have the original version of the vendor — the one from FindFirst(). This version no longer matches what’s in the database (because ProcA already modified and saved a different version). When we try to Modify() again, AL detects the version mismatch and throws the error.

The solution is straightforward — add var:

procedure ProcA(var Vendor: Record Vendor)
begin
    Vendor.Address := 'Hey';
    Vendor.Modify();
end;

Now both the caller and the procedure share the same record, and everything stays in sync.

The Plot Twist: .NET-Backed Types Ignore the Rules

Here’s where it gets surprising. Let’s try the same experiment with a JsonObject:

var
    j: JsonObject;
begin
    j.Add('Field1', 'A value');
    ProcA(j);
    Message(Format(j));
end;

procedure ProcA(j: JsonObject)
begin
    j.Add('Field2', 'Another value');
end;

Notice there is no var on the parameter. Based on everything discussed so far, and based on the official documentation, we would expect the message to show a JSON structure with only Field1. But what actually happens?

We get both fields. The JSON structure contains Field1 and Field2.

So what’s going on? The answer lies in how C# (which underpins AL) handles different types. JsonObject is not an AL primitive type — it’s a .NET object. In .NET, objects are reference types, meaning they are always passed by reference, regardless of whether you specify var or not.

This applies to other .NET-backed types as well: HttpClient, HttpResponseMessage, HttpContent, and similar types are always passed by reference.

The reason is practical: .NET objects can have complex internal structures — circular references, deep hierarchies, etc. Creating deep copies of these objects for every function call would be prohibitively expensive.

The Three Rules

To summarize, there are three categories to understand:

  1. Primitive types (Integer, Decimal, Date, Text, Code, etc.) — these are copied by default. You need var to pass by reference.
  2. Record types — these behave like classic AL types and are also copied by default. You need var to pass by reference.
  3. .NET-backed types (JsonObject, HttpClient, HttpResponseMessage, etc.) — these are always passed by reference, whether you specify var or not.

Erik recommends that even though var is technically unnecessary for .NET-backed types, you should still add it for readability. It signals to the reader of your code that you intend to modify the parameter and helps communicate how the function works.

Bonus: The Dangers of Overusing var

Erik points out a cautionary example from the base application — the Type Helper codeunit. Look at functions like UrlEncode and UrlDecode:

var
    t1, t2 : Text;
begin
    t1 := 'https://www.hougaard.com/something';
    t2 := t.UrlEncode(t1);
    OnAfterEncoding(t1, t2);
end;

These functions are marked with var on their input parameter, meaning they both return the encoded value and modify the input parameter with the encoded version. Unless you read the source code, you might not realize that t1 gets overwritten — which can lead to double-encoding bugs and other subtle issues.

This is a great example of why blindly adding var to everything is dangerous. It can create confusing, hard-to-debug side effects.

Bonus: Watch Your Event Parameters

Another thing to be aware of: when using AL code actions to generate integration event subscribers, the generated parameters do not include var by default:

[IntegrationEvent(false, false)]
local procedure OnAfterEncoding(t1: Text; t2: Text)
begin
end;

If you want subscribers to be able to modify the parameters, you need to manually add var to the appropriate parameters. Think carefully about which parameters should be modifiable — perhaps you want t1 to be changeable but not t2. The auto-generated code won’t make this decision for you.

Conclusion

The var keyword in AL is deceptively simple on the surface but carries significant implications for how your code behaves. The key takeaways are: primitive types and records follow the documented rules (copied by default, passed by reference with var), but .NET-backed types like JsonObject and HttpClient are always by reference regardless. Use var intentionally — add it when you need it, include it on .NET types for readability, but don’t slap it on everything or you risk creating confusing side effects like those found in the Type Helper codeunit.