When “Errors” Don’t Stop the Process: A Hidden Risk in Business Central

Even if you’re not a developer, it’s useful to know that in Business Central there are different ways the system can react when something goes wrong. Most people naturally assume: if an error happens, everything stops and nothing gets saved.

In many cases, that’s true – and it’s a good safety net.

But there’s a specific development feature in Business Central that can change this behavior. With it enabled, the system can continue processing even after errors, and successful changes may still be saved instead of being undone automatically.

This post is a development topic, but I’m writing it in plain language because it affects how predictable certain bulk actions, imports, or background processes are for end users.


Why I’m writing about this

I’m writing this because I ran into it during a code review.

A colleague used a pattern that looked harmless at first: “collect errors and show them at the end.” But the attribute was applied to a procedure that doesn’t only check — it also processes and changes data.

That’s the dangerous part: the code can keep going after errors, and you may end up with partial updates committed to the database. This post is my attempt to make that risk obvious (so we don’t accidentally ship it).


The feature: ErrorBehavior::Collect

In AL (the language Business Central developers use), you can decorate a procedure like this:

[ErrorBehavior(ErrorBehavior::Collect)]
procedure MyProcedure()
begin
    ...
end;

This changes how errors are handled.

Default behavior (normal)

  • An error occurs → execution stops
  • The error is raised immediately
  • The transaction rolls back (changes are undone)

With ErrorBehavior::Collect

  • Errors are collected instead of thrown immediately
  • The procedure continues even after an error
  • Multiple errors can be shown together at the end
  • No automatic rollback happens just because an error occurred

That last bullet is the key.


The real risk: “partial success” becomes possible

Once you allow processing to continue after an error, you’re effectively enabling a “partial success” model:

  • some records succeed and their changes are saved
  • some records fail and are reported as errors
  • the overall run finishes and returns the collected errors

If the code was designed for that on purpose (like an import), it can be great.
If not, it can create confusing states like:

  • some flags/statuses updated but dependent steps missing
  • part of a batch processed, part not
  • data that is technically saved but logically inconsistent

This is why using Collect directly on processing procedures can be risky.


The pattern Microsoft typically uses

A safer pattern you’ll often see in Microsoft code is:

  1. Check/validate first (collect all issues so the user sees everything at once)
  2. If checks are clean → process normally (fail-fast + rollback)

In practice, Microsoft tends to apply ErrorBehavior::Collect to check codeunits/procedures — not to the actual posting/processing procedure.

Here’s a typical example structure (validation-focused), where the check runs, and any errors are collected into an error buffer (codeunit 9078 “Check Item Jnl. Line. Backgr.”):

[ErrorBehavior(ErrorBehavior::Collect)]
local procedure RunItemJnlCheckLineCodeunit(ItemJnlLine: Record "Item Journal Line"; var TempErrorMessage: Record "Error Message" temporary)
var
    TempLineErrorMessage: Record "Error Message" temporary;
    ItemJnlCheckLine: Codeunit "Item Jnl.-Check Line";
begin
    if not ItemJnlCheckLine.Run(ItemJnlLine) then
        InsertTempLineErrorMessage(
            TempLineErrorMessage,
            ItemJnlLine.RecordId(),
            0,
            GetLastErrorText(),
            false);

    if HasCollectedErrors then begin
        CollectErrors(ItemJnlLine, TempLineErrorMessage);
        CopyErrorsToBuffer(TempLineErrorMessage, TempErrorMessage);
    end;
end;

What this means in plain language: this code is primarily used to validate (check) whether a journal line is OK. If something is wrong, it doesn’t immediately stop at the first issue — it gathers the errors into a temporary buffer so they can be shown together. Importantly, this kind of “collect errors” usage is much safer when it’s kept to checks rather than the processing/posting logic.

That’s also why it’s risky when someone applies the same attribute to procedures that both check and process — because then you can continue after an error and still commit partial updates.


Practical guidance

Use ErrorBehavior::Collect for:

  • pre-checks / validations (Check*, Validate*)
  • “tell me everything that’s wrong before we start”

Be very careful using it for:

  • Process() / Post() style procedures
  • anything that must be all-or-nothing (posting, ledger entries, multi-table updates)

Rule of thumb:
If partial completion would confuse users or risk data consistency, don’t apply Collect to the processing path.


Closing

ErrorBehavior::Collect is useful — but it’s not “just nicer error messages.” It changes the safety model from “stop and roll back” to “continue and commit what succeeded.”

That’s why I’m calling it a hidden risk: it can sneak into processing code during refactoring or good-intentioned error handling, and you only notice later when a batch leaves the system in a half-processed state.

If you want the benefit (collecting all errors), keep it in checks — and keep the real processing transactional unless you explicitly want partial success.

Leave a comment