Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions microsoft/knowledge/performance/avoid-commit-inside-loops.bad.al
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
codeunit 50129 "Perf Sample CommitInLoop Bad"
{
procedure ReleaseAllOrders()
procedure NormalizeCustomerNames()
var
SalesHeader: Record "Sales Header";
Customer: Record Customer;
begin
SalesHeader.SetRange(Status, SalesHeader.Status::Open);
if SalesHeader.FindSet() then
if Customer.FindSet(true) then
repeat
SalesHeader.Status := SalesHeader.Status::Released;
SalesHeader.Modify();
Customer.Name := UpperCase(Customer.Name);
Customer.Modify();
Commit();
until SalesHeader.Next() = 0;
until Customer.Next() = 0;
end;
}
21 changes: 21 additions & 0 deletions microsoft/knowledge/performance/avoid-commit-inside-loops.good.al
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
codeunit 50128 "Perf Sample CommitInLoop Good"
{
procedure NormalizeCustomerNames()
var
Customer: Record Customer;
RowsInChunk: Integer;
ChunkSize: Integer;
begin
ChunkSize := 500;
if Customer.FindSet(true) then
repeat
Customer.Name := UpperCase(Customer.Name);
Customer.Modify();
RowsInChunk += 1;
if RowsInChunk >= ChunkSize then begin
Commit();
RowsInChunk := 0;
end;
until Customer.Next() = 0;
end;
}
8 changes: 5 additions & 3 deletions microsoft/knowledge/performance/avoid-commit-inside-loops.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
bc-version: [all]
domain: performance
keywords: [commit, loop, transaction, lock]
keywords: [commit, loop, transaction, lock, checkpoint, codeunit-run]
technologies: [al]
countries: [w1]
application-area: [all]
Expand All @@ -13,11 +13,13 @@ application-area: [all]

## Description

Commit ends the current transaction. Calling it inside a loop produces one transaction per iteration and loses the ability to roll back the whole operation atomically. It also interferes with the platform's ability to batch write operations. The original motivation — releasing locks during a long batch — is better served by splitting the batch into explicit checkpoints that each process a bounded number of rows.
Commit ends the current write transaction. Calling it inside a per-row loop produces one transaction per iteration and loses the ability to roll back the whole operation atomically; it also interferes with the platform's ability to batch write operations. Most loops need no explicit Commit at all — AL auto-commits the enclosing code module on successful completion (see `understand-implicit-transaction-boundary.md`). When the batch is too large for one transaction, the fix is not a per-row Commit but bounded checkpoints that each process N rows.

## Best Practice

If the batch is large enough that a single transaction is untenable, process it in checkpoints driven by an outer loop that each time picks up the next N rows. Commit once per checkpoint at a clearly defined safe boundary, not inside the per-row loop.
If the batch is large enough that a single transaction is untenable, process it in checkpoints driven by an outer loop that each time picks up the next N rows. Commit once per checkpoint at a clearly defined safe boundary, not inside the per-row loop. Wrapping each chunk in `Codeunit.Run` gives the same effect with native rollback on failure — see `codeunit-run-as-atomic-sub-operation.md`.

See sample: `avoid-commit-inside-loops.good.al`.

## Anti Pattern

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
codeunit 50144 "Perf Sample AtomicSub Bad"
{
procedure ApplyDiscountToSelection(var Customer: Record Customer)
begin
if Customer.FindSet(true) then
repeat
Customer."Customer Price Group" := 'VIP';
Customer.Modify(true);
Commit();
until Customer.Next() = 0;
end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
codeunit 50142 "Perf Sample AtomicSub Good"
{
procedure ApplyDiscountToSelection(var Customer: Record Customer)
var
ApplyOne: Codeunit "Perf Sample Apply Discount";
begin
if Customer.FindSet() then
repeat
ClearLastError();
if not ApplyOne.Run(Customer) then
LogSkipped(Customer."No.", GetLastErrorText());
until Customer.Next() = 0;
end;

local procedure LogSkipped(CustomerNo: Code[20]; ErrorText: Text)
begin
end;
}

codeunit 50143 "Perf Sample Apply Discount"
{
TableNo = Customer;

trigger OnRun()
begin
Rec.Validate("Customer Price Group", 'VIP');
Rec.Modify(true);
end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
bc-version: [all]
domain: performance
keywords: [codeunit-run, atomic, rollback, transaction, sub-transaction, try-pattern, implicit-commit]
technologies: [al]
countries: [w1]
application-area: [all]
---

# Use Codeunit.Run to bound an atomic sub-operation

## Description

`Codeunit.Run(ID)` is the AL-idiomatic way to run a unit of work as an atomic sub-operation with its own transactional boundary. When the return value is captured — `if Codeunit.Run(MyCodeunit) then ...` — the runtime treats the codeunit as a unit: on successful completion it performs an implicit commit of the codeunit's database changes; on error it rolls those changes back and the caller receives `false`. Per the platform reference, "any changes done to the database will be committed at the end of the codeunit, unless an error occurs." The caller decides how to react — compensate, surface an error, continue — without having to manage transactions by hand.

## Best Practice

When a piece of work must either complete fully or have no effect, put it in its own codeunit and invoke it via `Codeunit.Run`, capturing the return. Use `if not Codeunit.Run(X) then Error(...)` to abort and unwind; use the plain boolean branch to react to failure without aborting the caller. This replaces the SQL-style `BEGIN TRAN / COMMIT / ROLLBACK` habit with a pattern the AL runtime implements natively. Do not confuse `Codeunit.Run` with `[TryFunction]` — both catch errors, but only `Codeunit.Run` rolls back database changes on failure (see `use-tryfunction-for-error-catching-not-rollback.md`). Note that if the caller is already in a write transaction, the platform requires a `Commit()` before `Codeunit.Run` — the sub-operation cannot nest inside an open transaction (see `codeunit-run-requires-prior-commit-inside-transaction.md`).

See sample: `codeunit-run-as-atomic-sub-operation.good.al`.

## Anti Pattern

Inlining the work in the caller and sprinkling `Commit()` to simulate sub-transaction boundaries. The caller's enclosing transaction is fused to the sub-work; any Commit between checkpoints survives subsequent errors, and any errors after a Commit cannot be cleanly unwound. Per-row Commits (see `avoid-commit-inside-loops.md`) are a frequent symptom.

See sample: `codeunit-run-as-atomic-sub-operation.bad.al`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
codeunit 50147 "Perf Sample OpenTxnRun Bad"
{
procedure ApplyDiscountToSelection(var Customer: Record Customer)
var
ApplyOne: Codeunit "Perf Sample OpenTxnRun Apply";
RunLog: Record "Custom Run Log";
begin
if Customer.FindSet() then
repeat
RunLog.Init();
RunLog."Customer No." := Customer."No.";
RunLog.Insert();
if not ApplyOne.Run(Customer) then;
until Customer.Next() = 0;
end;
}

codeunit 50148 "Perf Sample OpenTxnRun Apply"
{
TableNo = Customer;

trigger OnRun()
begin
Rec.Validate("Customer Price Group", 'VIP');
Rec.Modify(true);
end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
codeunit 50145 "Perf Sample DeferredLog Good"
{
procedure ApplyDiscountToSelection(var Customer: Record Customer)
var
ApplyOne: Codeunit "Perf Sample DeferredLog Apply";
FailedCustomerNos: List of [Code[20]];
FailureReasons: List of [Text];
Index: Integer;
begin
if Customer.FindSet() then
repeat
ClearLastError();
if not ApplyOne.Run(Customer) then begin
FailedCustomerNos.Add(Customer."No.");
FailureReasons.Add(GetLastErrorText());
end;
until Customer.Next() = 0;

for Index := 1 to FailedCustomerNos.Count() do
WriteFailureLog(FailedCustomerNos.Get(Index), FailureReasons.Get(Index));
end;

local procedure WriteFailureLog(CustomerNo: Code[20]; Reason: Text)
begin
end;
}

codeunit 50146 "Perf Sample DeferredLog Apply"
{
TableNo = Customer;

trigger OnRun()
begin
Rec.Validate("Customer Price Group", 'VIP');
Rec.Modify(true);
end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
bc-version: [all]
domain: performance
keywords: [codeunit-run, commit, write-transaction, nesting, loop, runtime-error]
technologies: [al]
countries: [w1]
application-area: [all]
---

# Commit before Codeunit.Run when the caller already holds a write transaction

## Description

`Codeunit.Run` cannot nest inside an open write transaction. Per the platform reference, "If you're already in a transaction you must commit first before calling `Codeunit.Run`." The platform enforces this at runtime: the first call dies with an error, not at compile time. The rule most often surfaces in a loop that pairs outer-scope writes — progress records, audit log entries, failure markers — with a per-item `Codeunit.Run`: the first outer write opens a transaction, the subsequent `Codeunit.Run` throws. `[CommitBehavior]` does not silence this, because the implicit commit inside `Codeunit.Run` is exempt from the attribute: "The `CommitBehavior` only applies to explicit commits, not implicit commits done as part of [Codeunit.Run]." `[TryFunction]` is not a substitute either: a try method catches errors but does not open its own rollback boundary (see `use-tryfunction-for-error-catching-not-rollback.md`).

## Best Practice

For the `Codeunit.Run` atomic-sub-operation pattern (see `codeunit-run-as-atomic-sub-operation.md`) to work in a loop, keep the outer scope **read-only**. Move per-iteration writes — progress updates, logging, audit entries — into the sub-codeunit so they commit or roll back together with the per-item work. If logging must live outside the atomic boundary, defer it: collect failure info in memory during the loop (a `List of [Text]`, a temporary record, local variables) and write it in one pass after the loop ends, when no outer write transaction is open.

See sample: `codeunit-run-requires-prior-commit-inside-transaction.good.al`.

## Anti Pattern

Inserting `Commit()` before each `Codeunit.Run` to silence the runtime error. The error goes away, but the outer scope now commits per iteration — the behavior `avoid-commit-inside-loops.md` exists to warn against. Attempting to silence the implicit commit inside the sub-codeunit with `[CommitBehavior(CommitBehavior::Ignore)]` also fails: the attribute does not apply to `Codeunit.Run`'s implicit commit. Conditioning the Commit on `Database.IsInWriteTransaction()` (runtime 11.0+) is another version of the same trap — the method has legitimate uses for diagnostics and library code that genuinely cannot control its caller, but branching production flow on runtime transaction state typically signals unclear ownership that would be better fixed by restructuring the caller so transaction state is predictable.

See sample: `codeunit-run-requires-prior-commit-inside-transaction.bad.al`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
bc-version: [all]
domain: performance
keywords: [commit, transaction, implicit-commit, write-transaction, runtime, boundary]
technologies: [al]
countries: [w1]
application-area: [all]
---

# AL auto-commits when code execution completes

## Description

In AL, write transactions are managed by the runtime, not by the developer. When AL code begins executing from an entry point — an outermost trigger, a codeunit invoked via `Codeunit.Run`, a report, a page action — the runtime opens a write transaction on the first database write. When that execution completes without error, the runtime commits automatically; if that execution errors, uncommitted writes are rolled back. Explicit `Commit()` is not how write transactions are *started*; it is how a single execution is *split* into multiple transactions. Per the platform reference, "The Commit method separates write transactions in an AL code module."

## Best Practice

Default to no explicit `Commit()`. Let the runtime open and close the transaction around the execution. Reach for `Commit()` only when the execution has a real reason to persist partial progress — for example, a long batch that must release locks between checkpoints (see `avoid-commit-inside-loops.md`), or work that calls an external service and must persist the resulting handle before continuing with operations that may fail independently. If a stretch of work needs to either complete fully or have no effect, prefer `Codeunit.Run` over manual Commit choreography (see `codeunit-run-as-atomic-sub-operation.md`).

## Anti Pattern

Sprinkling `Commit()` defensively — at the end of a procedure, after every Modify, or "just to be safe" — reflects a SQL-style mental model that does not apply here. Every stray Commit shortens the rollback window: work before the Commit survives later errors the developer almost certainly intended to unwind. A Commit without a specific reason is a bug waiting to surface.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
codeunit 50156 "Perf Sample TryFunc Bad"
{
procedure ApplyDiscountAttempt(var Customer: Record Customer)
begin
if not TryApplyDiscount(Customer) then
Message('Discount not applied');
end;

[TryFunction]
local procedure TryApplyDiscount(var Customer: Record Customer)
begin
Customer.Validate("Customer Price Group", 'VIP');
Customer.Modify(true);
if Customer."Credit Limit (LCY)" <= 0 then
Error('Customer %1 not eligible', Customer."No.");
end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
codeunit 50155 "Perf Sample TryFunc Good"
{
procedure ParseAndProcess(Payload: Text)
var
ParsedValue: Decimal;
begin
ClearLastError();
if not TryParseDecimal(Payload, ParsedValue) then begin
LogParseFailure(Payload, GetLastErrorText());
exit;
end;
end;

[TryFunction]
local procedure TryParseDecimal(Input: Text; var Result: Decimal)
begin
Evaluate(Result, Input);
end;

local procedure LogParseFailure(Payload: Text; Reason: Text)
begin
end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
bc-version: [all]
domain: performance
keywords: [try-function, try-method, error-handling, rollback, atomic, exception, get-last-error, session-buffer]
technologies: [al]
countries: [w1]
application-area: [all]
---

# Use [TryFunction] for error catching, Codeunit.Run for atomic rollback

## Description

`[TryFunction]` annotates a method so that errors raised inside it can be caught by the caller instead of propagating. Per the platform reference, "changes to the database that are made with a try method aren't rolled back" — the attribute catches the error; it does not unwind database state. This is the critical distinction from `Codeunit.Run`, which does roll back on error (see `codeunit-run-as-atomic-sub-operation.md`). A try function also only catches when its return value is used: "If the return variable for a call to a function, which is attributed with [TryFunction] isn't used, then the call isn't considered a try function call." `DoTry();` propagates errors normally; only `ok := DoTry();` or `if DoTry() then ...` catches. The return type is forced to Boolean; user-defined return types are not allowed, and the value isn't accessible inside the try method itself. On Business Central on-premises, writes inside a try method are blocked by default and raise a runtime error unless `DisableWriteInsideTryFunctions` is set to `false` on the server — SaaS has no such restriction.

## Best Practice

Reach for `[TryFunction]` when you want to catch a failure without unwinding the transaction — HTTP calls whose non-2xx responses should surface a user-friendly message, .NET interop whose exceptions you want to translate, validation or parsing routines whose errors you intend to log and continue past. Always capture the return: `if MyTry() then ... else HandleFailure(GetLastErrorText());`. When the work is transactional — writes that must either fully apply or fully revert — use `Codeunit.Run` instead. The two primitives solve different problems: one catches errors, the other bounds a rollback scope.

Use `[TryFunction]` sparingly. Each caught error writes to the session-wide `GetLastErrorText` and `GetLastErrorCallStack` buffers, and every subsequent catch overwrites the earlier state — a helper that reads `GetLastErrorText` later may see a different error than the one it intended to inspect. Prefer explicit checks (non-throwing predicates, guard conditions, upfront validation) for operations with predictable failure modes; reserve `[TryFunction]` for genuinely unpredictable failures such as network calls, third-party interop, or evaluation of user-supplied expressions. When you do catch, read `GetLastErrorText` immediately after the failed call, and call `ClearLastError` before the call if an earlier catch in the same scope could have left state behind — per the platform reference, "If you call the GetLastErrorText method immediately after you call the ClearLastError method, then an empty string is returned."

See sample: `use-tryfunction-for-error-catching-not-rollback.good.al`.

## Anti Pattern

Wrapping database writes in `[TryFunction]` expecting the writes to roll back when the method errors. They do not: the writes that succeeded before the error remain, the caller receives `false`, and the corrupted-state bug surfaces in production. A related anti-pattern is calling a try function without capturing the return (`DoTry();`), which silently strips the error-catching behavior and lets the error propagate — the code looks defensive but behaves identically to an unwrapped call. A third is defensive sprinkling: wrapping every operation that *could* theoretically error in `[TryFunction]` on the theory that catching is always safer than propagating. Each extra catch pollutes the shared error buffer and makes the diagnostic signal harder to find when something real does fail.

See sample: `use-tryfunction-for-error-catching-not-rollback.bad.al`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
codeunit 50151 "Sec Sample CommitBeh Bad"
{
[IntegrationEvent(true, false)]
procedure OnBeforeApplyingDiscount(var Customer: Record Customer)
begin
end;

procedure ApplyDiscount(var Customer: Record Customer)
begin
Customer."Customer Price Group" := 'VIP';
Customer.Modify(true);
OnBeforeApplyingDiscount(Customer);
if Customer."Credit Limit (LCY)" <= 0 then
Error('Customer %1 not eligible', Customer."No.");
Commit();
end;
}

codeunit 50152 "Sec Sample CommitBeh Bad Sub"
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sec Sample CommitBeh Bad", 'OnBeforeApplyingDiscount', '', true, true)]
local procedure NotifyOther(var Customer: Record Customer)
begin
Commit();
end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
codeunit 50149 "Sec Sample CommitBeh Good"
{
[CommitBehavior(CommitBehavior::Ignore)]
[IntegrationEvent(true, false)]
procedure OnBeforeApplyingDiscount(var Customer: Record Customer)
begin
end;

procedure ApplyDiscount(var Customer: Record Customer)
begin
Customer."Customer Price Group" := 'VIP';
Customer.Modify(true);
OnBeforeApplyingDiscount(Customer);
if Customer."Credit Limit (LCY)" <= 0 then
Error('Customer %1 not eligible', Customer."No.");
Commit();
end;
}

codeunit 50150 "Sec Sample CommitBeh Good Sub"
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sec Sample CommitBeh Good", 'OnBeforeApplyingDiscount', '', true, true)]
local procedure NotifyOther(var Customer: Record Customer)
begin
Commit();
end;
}
Loading