ELI5 Defensive programming in AL

In this video I take a look at defensive programming in AL, explain-it-Like-I’m-5 style. How do we approach defensive programming with practical examples. Check out the video:

https://youtu.be/WELkmKnTGfM

In this video, Erik walks through the core principles of defensive programming in AL for Business Central. Using a hands-on demo, he covers input validation, assertions, error handling with try functions, boundary checks, and how the AL platform already provides a safety net for many common programming pitfalls.

What Is Defensive Programming?

The opposite of defensive programming is what you might call optimistic programming — you assume everything goes right. You develop your software for one lucky path where everything is entered into the system in the right way, in the right order, on the right day, and the moon is in the right position. If any of those things fail, everything blows up.

With a defensive programming mindset, you assume the moon is not in the right spot, it’s Tuesday, and the user decides to fill in the last field on the screen first. You plan for the unexpected.

The AL Platform Has Your Back (Mostly)

An important starting point: Microsoft has created the AL platform in a way that handles many of the issues other programming environments struggle with — memory management, race conditions, threading, and more. If you forget to close a file, AL closes it for you. If you forget to free memory, it gets freed automatically.

We live in this protective world where we can concentrate on business logic and actually adding value. But even inside this warm, fuzzy place, there are still things we need to consider.

The Six Principles of Defensive Programming in AL

Erik identifies six key areas to think about, some of which overlap:

  1. Input Validation — Ensuring data meets expected criteria before processing
  2. Assertions — Checking conditions that should always be true
  3. Error Handling — Gracefully managing exceptions using try functions
  4. Boundary Checks — Validating array indices and string lengths
  5. Principle of Least Privilege — Granting only necessary permissions in code
  6. Failsafe Defaults — Defaulting to a safe state when errors occur

Failsafe Defaults: The AL Transaction Model

The failsafe default principle is largely given to us by Microsoft. Whenever something happens in Business Central, a transaction starts in the database. If there’s an error, the system rolls back to the point where the transaction started — the “last known idle state” in the UI. If you run a report and it fails, you go back to where the report was called from. If you type in a field and there’s an error, the data reverts to what it was before you typed, though the cursor remains on the field with the error.

Input Validation in Practice

Code Fields Are Smart

A common scenario: a user copies and pastes text into a field, and the pasted value includes a trailing space. For Code fields in Business Central, this is handled automatically — spaces are trimmed and text is uppercased. The Code data type is more than just an uppercasing text field; it’s inherently smart about cleaning input.

Adding a Custom Field with Proper Validation

Erik demonstrates adding a new Code[20] field called ImportantAccount to the Customer Posting Group table. Without proper configuration, you could type anything into the field — including nonsensical values like “Peter” for an account number.

The solution is to layer on validation using built-in AL features and custom logic:

field(50100; ImportantAccount; Code[20])
{
    Caption = 'Important Account';
    DataClassification = SystemMetadata;
    TableRelation = "G/L Account"."No." where("Account Type" = const(Total));
    NotBlank = true;
    trigger OnValidate()
    var
        GL: Record "G/L Account";
        TGC: Record "Tax Group";
    begin
        GL.Get(ImportantAccount);
        if not (GL."Account Type" = GL."Account Type"::Total) then
            error('Not Total account type');
        if GL.Name.Contains('A') then
            error('We do not want that account');
        if not TGC.Get(GL."Tax Group Code") then;
    end;
}

Key validation layers at work here:

  • TableRelation — Ensures the value exists in the G/L Account table and filters by account type
  • NotBlank — Prevents empty values
  • OnValidate trigger — Adds custom business logic checks on top of the built-in validation

Assertions: Double-Checking Assumptions

Notice the assertion in the OnValidate trigger: even though the TableRelation already filters for Account Type = Total, the code explicitly checks it again. This is a defensive assertion — confirming that something which should always be true actually is true. The table relation helps in the UI, but the assertion in the OnValidate trigger protects against code-level assignments that might bypass the UI.

Protected vs. Unprotected Get

Erik makes an important distinction: you should generally never have an unprotected Get (one that throws an error if the record isn’t found). However, in the OnValidate trigger above, the unprotected GL.Get(ImportantAccount) is actually fine because Microsoft’s table relation validation has already confirmed the record exists before the trigger fires.

For the Tax Group Code lookup, a protected pattern is used instead:

if not TGC.Get(GL."Tax Group Code") then;

This gracefully handles the case where the tax group might not exist, without throwing an error.

Handling Data Imports: Validate and CopyStr

When importing data programmatically, you face additional challenges that don’t exist in the UI. Erik highlights two critical issues:

Problem 1: String Length Mismatch

A Text variable can hold up to a billion characters, but the ImportantAccount field is only Code[20]. Use CopyStr with MaxStrLen to safely truncate:

CPG.validate(ImportantAccount, CopyStr(x, 1, MaxStrLen(CPG.ImportantAccount)));

The MaxStrLen function returns the maximum length of the field (20 in this case), and CopyStr takes only the first 20 characters from the input.

Problem 2: Direct Assignment Skips Validation

Assigning directly to a field (e.g., CPG.ImportantAccount := x;) does not trigger the OnValidate logic. You must use the Validate command instead, which simulates user input and runs all validation triggers:

CPG.Validate(ImportantAccount, CopyStr(x, 1, MaxStrLen(CPG.ImportantAccount)));

Error Handling with Try Functions

If the validation triggers an error during an import, the entire process stops and rolls back to the last known idle state. But what if you’re importing 15 records and only record 7 is bad? You want to log the failure and continue processing records 1–6 and 8–15.

This is where try functions come in:

[TryFunction]
procedure insertstuff()
var
    CPG: Record "Customer Posting Group";
    x: Text;
begin
    CPG.Init();
    CPG.Validate(ImportantAccount, CopyStr(x, 1, MaxStrLen(CPG.ImportantAccount)));
    CPG.Insert();
end;

A try function returns a boolean indicating success or failure. You call it and handle the result:

repeat
    result := insertstuff();   
    if not result then begin
        // Insert failure log
        // GetLastErrorText() returns the error message
    end;
until Done;

The GetLastErrorText() function returns the error message, so you can log exactly what went wrong — for example, “Important Account could not be ‘Peter'” — and continue processing.

Critical Rules for Try Functions

  • Only one database operation per try function. If you have two inserts and the second one fails, the first one has already succeeded. Since the try function catches the error instead of rolling back, you’d end up with an inconsistent database state.
  • The database operation should be the last thing in the function. Any code after a failed operation won’t execute, so put your database write at the end.
  • Try functions cannot have a return value because the boolean return is reserved for indicating success/failure. Use var parameters for output instead.

Boundary Checks: The Date Example

While buffer overruns are nearly impossible in AL, boundary issues can still cause errors. Erik demonstrates with date construction:

DayNo := 29;
Month := 2;
Year := 2001;

// This will fail: February 2001 only has 28 days (not a leap year)
d := DMY2Date(DayNo, Month, Year);

Microsoft’s DMY2Date function has its own built-in validation — it throws an error if the day is outside the permitted range for the given month and year. But if you want to handle this gracefully instead of crashing, wrap it in a try function:

[TryFunction]
procedure CreateDate(d: integer; m: integer; y: integer; var outdate: Date)
begin
    outdate := DMY2Date(d, m, y);
end;

Now you can attempt date creation safely:

if not CreateDate(DayNo, Month, Year, D) then
    Message('Wrong Date!');

No crash, no rollback — just a graceful notification that the date was invalid, and your code continues running.

The Page Extension: Exposing the Field

For completeness, the new field is exposed on the Customer Posting Groups page:

pageextension 50101 CPG extends "Customer Posting Groups"
{
    layout
    {
        addafter(Code)
        {
            field(ImportantAccount; Rec.ImportantAccount)
            {
                ApplicationArea = all;
            }
        }
    }
}

Summary

Defensive programming in AL comes down to never assuming the happy path. While the AL platform and Business Central’s transaction model provide a significant safety net — automatic rollbacks, memory management, and type safety — there’s still plenty you need to handle yourself:

  • Validate all input using table relations, field properties like NotBlank, and OnValidate triggers
  • Use Validate() instead of direct assignment when setting field values programmatically
  • Protect your Get calls unless you’re certain the record exists
  • Use CopyStr and MaxStrLen to prevent string overflow when importing external data
  • Use try functions to gracefully handle errors without stopping entire processes — but keep them focused on a single database operation
  • Add assertions to verify conditions that should always be true, catching logic errors early

The goal is to catch problems as early as possible, handle them gracefully, and keep the system in a consistent state — no matter what the user (or the moon) throws at you.