← All postsDevelopment ยท 3 November 2026

Error handling in AL: ErrorInfo, collectible errors, TryFunctions and the transaction

An error in AL rolls back the transaction, stops the user, and shows a message. Done well it tells them exactly what to fix and offers to fix it. Done badly it is "an error occurred" or, worse, a TryFunction that swallowed a posting failure. The modern error model, when to use each tool, and the rules about Commit that every developer should be able to recite.

Error handling in AL is simpler than in most languages, because the platform does most of it: an unhandled Error rolls back the whole transaction, ends the operation and shows the user the message. The decisions a developer makes are about what the message says, whether to collect several errors before stopping, when it is legitimate to catch one, and what state the database is in afterwards. Getting those right is the difference between a system users trust and one they work around.

Error with ErrorInfo: say what to do, and offer to do it

Since version 20, Error takes an ErrorInfo rather than just text. It carries a title, a message, a detailed message, a verbosity, a data classification, a record to navigate to, custom dimensions for telemetry, and actions: buttons on the error dialog that run a procedure to fix the problem.

var
    PalletMissingTitleErr: Label 'Pallet configuration missing';
    PalletMissingErr: Label 'Item %1 has no pallet configuration, so it cannot be despatched.', Comment = '%1 = Item No.';
    PalletMissingDetailErr: Label 'Set the pallet configuration on the item card, or ask stock control to.';
    OpenItemCardLbl: Label 'Open item card';
    ShowItemLbl: Label 'Show item';
    ItemNoTok: Label 'ItemNo', Locked = true;

procedure CheckPalletConfig(Item: Record Item)
var
    Info: ErrorInfo;
begin
    if Item."CON Pallet Configuration"  '' then
        exit;
    Info.Title := PalletMissingTitleErr;
    Info.Message := StrSubstNo(PalletMissingErr, Item."No.");
    Info.DetailedMessage := PalletMissingDetailErr;
    Info.RecordId := Item.RecordId;
    Info.AddAction(OpenItemCardLbl, Codeunit::"CON Error Actions", 'OpenItemCard');
    Info.AddNavigationAction(ShowItemLbl);
    Info.CustomDimensions.Add(ItemNoTok, Item."No.");
    Error(Info);
end;

The user sees a titled dialog with a button that opens the item card. That is a support ticket that never gets logged (see the ten you do not need). The rules for the text: say what is wrong, on which record, and what to do about it; name the field and the value; never "invalid data" or "an error occurred". And every piece of text the user can see is a Label, never a string literal in the code: Microsoft's guidance and the CodeCop analyser both require it, it is what makes the text translatable through the XLIFF file, and it keeps the wording in one place. Labels carry a Comment describing each placeholder, and technical strings that must not be translated (telemetry event IDs, dimension names, token values) are labels with Locked = true.

Collectible errors: report all of them, not the first

A validation that stops at the first problem on a 200-line document sends the user round the loop 200 times. Mark the procedure [ErrorBehavior(ErrorBehavior::Collect)] and raise errors with Info.Collectible := true; they are collected instead of thrown, and at the end of the procedure (or when you call HasCollectedErrors / GetCollectedErrors) they are shown together, each with its own navigation. The user fixes all of them in one pass. Use it for pre-posting checks, import validation, and anything batch-shaped.

TryFunctions: catch, but only when you mean it

A procedure marked [TryFunction] returns false instead of throwing when an error occurs inside it. It is the only way to catch an error in AL, and it is widely misused. The right uses: calling an external service that may fail (an HTTP call, a file parse) where you want to log and continue; probing whether something would succeed; isolating one record's failure in a batch so the others proceed. The wrong use: wrapping a posting routine, swallowing the error, and carrying on as if it worked.

The reason it is dangerous: a TryFunction does not roll back the database changes made inside it before the error. If the code inside wrote three records and then failed, those three records are still in the transaction. If you then Commit, they are in the database. Microsoft blocks TryFunctions that would leave the database inconsistent in some cases, but not all. So: never write to the database inside a TryFunction unless you handle the partial state yourself, and if the TryFunction returns false, treat the transaction as dirty: either error out properly or roll back by erroring.

The transaction, and Commit

Everything a user action does, including every event subscriber it triggers, runs in one transaction; an error anywhere rolls all of it back. This is a feature: posting is all or nothing because of it. Commit ends the transaction and starts a new one, which means everything before it is permanent even if everything after it fails. The rules:

Locks, retries and the user

Lock timeouts ("another user has locked...") are errors like any other; the platform retries some automatically. Design so they are rare: read with FindSet(), lock late and narrow, keep transactions short. When a user hits one, the message should say who holds it where possible; LockTimeout errors carry the AL stack in telemetry so the support desk can see the contention (telemetry post).

Validation versus error

Not every problem is an error. A Confirm lets the user decide ("Credit limit exceeded. Continue?"). A notification (the blue bar) warns without stopping, with actions attached, for things the user should know and may fix later. A field validation error on the card stops the bad value at entry, which is better than stopping it at posting. Use the mildest tool that protects the data; users who see a red dialog ten times a day stop reading them.

The checklist

  1. Every error names the record, the field and the fix; most carry an action.
  2. Batch validations collect errors.
  3. TryFunctions only around external calls and per-record isolation, never around posting, and never with unhandled database writes inside.
  4. No Commit without a written reason; none in subscribers or loops.
  5. Validate in OnBefore events and field triggers, not in OnAfter.
  6. Every unexpected error path logs telemetry with an ID.
  7. Confirm and notifications for things that are not errors.

Error handling is where an extension's quality is most visible to users and least visible in a demo. It is also most of what separates a system that is trusted from one that is merely installed.

Talk to us
Want this done in your Business Central?
Book a 30 minute callGet the monthly newsletter

More from the blog