Throwing multiple errors with BC Version 19

Come join me, as I venture into the new ErrorInfo data type, and see how to use the new options for throwing errors Business Central.

https://youtu.be/8_ltR1ceTKA

In this video, Erik explores the ErrorInfo data type introduced in Business Central AL version 19 — one of the most significant additions to the AL language in decades. This is the first time the Error function has gained new functionality since the very first version of AL (then C/AL) over 30 years ago. Erik experiments live with creating enriched error messages, collecting multiple errors, and examines how these features interact with try functions.

The ErrorInfo Data Type

AL version 19 introduces a new data type called ErrorInfo. This type allows developers to create rich, structured error objects that go far beyond the simple text messages we’ve been limited to for three decades.

The basic usage starts with the static Create method:

var
    e1: ErrorInfo;
begin
    e1 := ErrorInfo.Create('This is the first error!');
    Error(e1);
end;

The Error function now has an overload that accepts an ErrorInfo variable instead of just a text string with format placeholders. This is the first change to the Error function signature that AL developers have seen in three decades.

Enriching Errors with Custom Dimensions

One of the powerful features of ErrorInfo is the ability to attach custom dimensions — additional metadata that flows into logs and telemetry. This is done using a Dictionary of [Text, Text]:

var
    e1: ErrorInfo;
    extraInfo: Dictionary of [Text, Text];
begin
    extraInfo.Add('Info1', 'Ordered by Erik');
    extraInfo.Add('Mood', 'Good');

    e1 := ErrorInfo.Create('This is the first error!');
    e1.CustomDimensions := extraInfo;
    Error(e1);
end;

Erik notes — with some amusement — that now that Microsoft has to support their own software in the cloud, there’s been a much stronger focus on telemetry, error tracing, and diagnostic information. Back in the on-premises days, all developers had was perhaps something in the event log.

Additional ErrorInfo Properties

Beyond custom dimensions, ErrorInfo supports several other properties:

  • DetailedMessage — Extra detailed information that doesn’t display to the user but goes into telemetry
  • DataClassification — For GDPR/PII compliance, allowing you to classify customer-sensitive information
  • ErrorType — Either Client or Internal, indicating who needs to see the error
  • Verbosity — Can be set to Warning, Normal, Error, or Critical
  • RecordId — The record associated with the error
  • SystemId — The system ID of the related record
  • FieldNo — The field number related to the error
  • PageNo — The page number related to the error

Erik tested changing the verbosity between Warning and Critical and found that the icon displayed to the user doesn’t change — all the enriched information flows into logs and telemetry rather than being exposed in the user-facing error dialog.

Collectible Errors: Throwing Multiple Errors

This is where things get truly interesting. Traditionally, calling Error immediately stops execution. With the new Collectible property and the ErrorBehavior attribute, you can now collect multiple errors before surfacing them.

There are two pieces required to make this work:

  1. Mark the error as collectible: e1.Collectible := true;
  2. Mark the procedure with the [ErrorBehavior(ErrorBehavior::Collect)] attribute
[ErrorBehavior(ErrorBehavior::Collect)]
procedure Test()
var
    e1: ErrorInfo;
    e2: ErrorInfo;
    extraInfo: Dictionary of [Text, Text];
begin
    extraInfo.Add('Info1', 'Ordered by Erik');
    extraInfo.Add('Mood', 'Good');

    e1 := ErrorInfo.Create('This is the first error!');
    e1.Collectible := true;
    e1.Verbosity := Verbosity::Critical;
    e1.CustomDimensions := extraInfo;
    Error(e1);

    // This code EXECUTES — even after calling Error!
    e2 := ErrorInfo.Create('This is the second error!');
    e2.Collectible := true;
    e2.Verbosity := Verbosity::Warning;
    e2.CustomDimensions := extraInfo;
    Error(e2);

    Message('I''m still alive!');
end;

When this runs, something remarkable happens: the code after the first Error call continues to execute. The message “I’m still alive” actually displays. Then, once the procedure’s scope ends, the collected errors are thrown together. The error dialog shows: “Multiple errors occurred during the operation, the first of which is: This is the first error!” — and the detail section lists all collected errors.

Error Collection Scope

An important discovery Erik made during testing: the collection scope applies to the entire call stack beneath the procedure marked with [ErrorBehavior(ErrorBehavior::Collect)]. If you call a sub-procedure that throws an error — even one that isn’t explicitly marked as collectible — it still gets collected, because the calling context has the collect error behavior.

[ErrorBehavior(ErrorBehavior::Collect)]
procedure Test()
var
    e1: ErrorInfo;
    e2: ErrorInfo;
begin
    e1 := ErrorInfo.Create('This is the first error!', true);
    Error(e1);

    e2 := ErrorInfo.Create('This is the second error!', true);
    Error(e2);

    Test2(); // This error also gets collected!

    Message('I''m still alive!');
end;

procedure Test2()
var
    e3: ErrorInfo;
begin
    e3 := ErrorInfo.Create('This is the third error!', true);
    Error(e3);
end;

The debugger confirms this: the break happens at the point where the Test() procedure returns to its caller — that’s where all the collected errors are finally thrown together.

Using the Compact Create Syntax

Instead of setting each property individually, you can pass many of them directly to the ErrorInfo.Create method as optional parameters:

e3 := ErrorInfo.Create(
    'This is the third error!',
    true,                              // Collectible
    // RecordId, FieldNo, PageNo, ControlName
    Verbosity::Critical,
    DataClassification::CustomerContent
);

Interaction with Try Functions

In the bonus section, Erik tested how collectible errors interact with try functions:

[TryFunction]
procedure TryTest()
begin
    Test(); // Test has ErrorBehavior::Collect and throws multiple errors
end;

// Calling code:
if not TryTest() then
    Message(GetLastErrorText());

The result: GetLastErrorText() returns only the summary message — “Multiple errors occurred during the operation, the first of which is: This is the first error!” — but does not include the detailed list of all collected errors. The GetCollectedErrors() function also returned a count of zero when called outside the collect scope, suggesting that collected error details are only accessible within the collection scope itself.

Source Code: Actions with Error Navigation

The source code provided shows a more advanced version of ErrorInfo usage (from version 22), including navigation actions and fix-it actions on errors:

pageextension 50100 CustomerListExt extends "Customer Card"
{
    actions
    {
        addfirst(processing)
        {
            action(Test)
            {
                ApplicationArea = All;
                Caption = 'Test';
                Image = Test;
                Promoted = true;
                PromotedCategory = Process;
                PromotedIsBig = true;
                ToolTip = 'Test';

                trigger OnAction()
                var
                    e: ErrorInfo;
                    Helpful: Codeunit Helpful;
                begin
                    e.Message := 'Not that fancy';
                    e.PageNo := 21;
                    e.FieldNo := Rec.FieldNo(Address);
                    e.RecordId := Rec.RecordId;
                    e.AddNavigationAction('Go North');
                    e.AddAction('Go South', 50100, 'Behelpful');
                    Helpful.SetContext(Rec.SystemId);
                    error(e);
                end;
            }
        }
    }
}

The AddNavigationAction method adds a clickable link in the error dialog that navigates to the relevant record, while AddAction ties a button to a codeunit method that can perform a corrective action:

codeunit 50100 "Helpful"
{
    SingleInstance = true;

    procedure Behelpful(e: ErrorInfo)
    var
        C: Record Customer;
    begin
        C.GetBySystemId(RememberedSystemId);
        message('Wasn''t that helpful? %1', C.Name);
    end;

    procedure SetContext(g: Guid)
    begin
        RememberedSystemId := g;
    end;

    var
        RememberedSystemId: Guid;
}

Summary

The ErrorInfo data type in Business Central AL version 19+ represents the first major enhancement to error handling in the language’s 30+ year history. Key takeaways:

  • Structured errors — Errors can now carry custom dimensions, data classifications, verbosity levels, and detailed messages for telemetry
  • Collectible errors — By combining the Collectible property with the [ErrorBehavior(ErrorBehavior::Collect)] attribute, multiple errors can be accumulated and presented together
  • Scope matters — The collection scope cascades down the call stack; sub-procedures inherit the collect behavior from their callers
  • Telemetry-first design — Much of the enriched error information (detailed messages, custom dimensions, verbosity) flows into Application Insights and telemetry rather than being displayed directly to users
  • Actionable errors — In later versions, errors can include navigation actions and fix-it actions that users can click directly from the error dialog