bcquality/microsoft/knowledge/error-handling/log-writes-must-survive-rollback.md
Michael Dieringer 67962727f6 Fix ten focused correctness items plus sample links from Jesper's 2026-09-15 re-review
Six carried-over threads:
- api-page-least-privilege-write-access fixtures: added the mandatory
  EntityName/EntitySetName properties (AL0485).
- pages-must-not-contain-business-logic fixtures: Sales Line has no
  "Total Amount" field; replaced with the real "Line Amount" (field 103).
- test-feature-scenario-tags.good.al and test-one-when-per-test.good.al:
  CreatePriceHeader leaves a price list in Draft status, which price
  calculation ignores. Added Validate(Status, Active) + Modify before the
  sales line that depends on it. Verified Status field/enum against
  PriceListHeader.Table.al and PriceStatus.Enum.al in the BCApps clone.
- exposed-objects-must-be-in-a-permission-set.md: a published codeunit is
  a SOAP endpoint (SOAP is deprecated), not OData - Page/Query are the
  OData object types. Corrected and pointed new integrations at API
  pages/queries instead.
- al-error-handling-review.md: the log-writes-must-survive-rollback cue
  selected on Session.StartSession, which only appears in the compliant
  fix, never in the anti-pattern - the bad fixture could never be
  worklisted. Recued on the actual risk shape (log insert around a
  failed TryFunction/GetLastError* path, then raise/propagate), with
  StartSession as an explicit compliant discriminator instead.
- page-design-must-match-bc-page-type-conventions.md: the enum value is
  NavigatePage, not Navigate; noted the type list is a selected subset,
  not an exhaustive PageType catalogue (PromptDialog, ConfigurationDialog,
  UserControlHost, XmlPort also exist, out of this article's scope).

Four new correctness gaps:
- release-must-update-app-version.md: "the version is the only identity"
  was backwards - id is the app's stable identity, version identifies a
  release/code-state of it.
- defensive-vs-offensive-code-must-match-blast-radius.good.al: the "low
  blast radius" example had no else branch, so a failed Customer.Get()
  left the field at its prior/default value instead of the explicit
  chosen fallback the article claims to demonstrate. Added the else.
- bcpt-scenarios-must-be-app-specific.good.al: InitTest and both measured
  StartScenario/EndScenario sections were empty/comment-only, so the
  "app-specific" fixture measured no actual work. Filled in a real,
  self-contained header+line creation path.
- upgrade-tag-logic-must-not-nest-deeply.good.al: the flattened version
  dropped both safety conditions the bad fixture had (Discount % = 0,
  nonblank posting group), silently changing behavior instead of just
  removing nesting. Extracted the guarded update into a helper with both
  conditions preserved as early exits.

Also converted this PR's remaining plain-backtick "See sample:" sample
references (16 articles) to the READ-convention markdown-link form,
matching the fix already made on #156/#158.

Rebased onto upstream/main (conflicts in al-ui-review.md, al-style-review.md,
al-upgrade-review.md against merged upstream PRs - all additive, both
sides' worklist cues retained).
2026-09-21 22:44:17 +02:00

2.4 KiB

bc-version domain keywords technologies countries application-area
all
error-handling
logging
rollback
session
transaction
isolated-session
telemetry
al
w1
all

Log writes that must capture failures must survive transaction rollback

Description

Inserting a log record inside the same transaction as the operation it logs looks correct until the operation errors: the transaction rolls back and takes the log entry with it. The result is a log that faithfully records every success and silently loses exactly the failures it exists to capture. This is a common blind spot in error/duration logging around web-service calls, background jobs, and other operations expected to fail sometimes.

Best Practice

Write any log whose purpose includes capturing failures from a transaction that is independent of the operation being logged: start an isolated session (Session.StartSession on a TableNo-scoped codeunit that only inserts the log record and commits) so the entry persists regardless of what happens to the caller's transaction. StartSession's only channel for getting data into that new session is its optional Record parameter, delivered to the target codeunit's OnRun trigger — a separate session gets a fresh instantiation of the codeunit, so calling a setter procedure on a local object variable before starting the session does not populate anything in the new session's instance. Capture duration and other telemetry values in the caller, place them into the Record passed to StartSession, and do the insert-and-commit entirely inside that session's own OnRun. Logs that only record successful, committed work can safely stay in the main transaction; this pattern targets error and diagnostic logs specifically.

See sample: log-writes-must-survive-rollback.good.al.

Anti Pattern

Inserting the error-log record in the same transaction as the risky operation, so a rollback deletes the very entry meant to explain the failure. Adding a stray Commit before the risky call is not a fix either — it breaks the caller's atomicity and can violate posting-routine rules. Swallowing the error to keep the log alive (running a codeunit without checking or re-raising its result) is equally wrong: the log must observe the failure, not suppress it.

See sample: log-writes-must-survive-rollback.bad.al.