Promote validated community knowledge (#105)

Move eight net-new rules into the Microsoft layer, remove six overlapping articles, and update review skill discovery and references.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0b130227-d418-4bc0-9e7d-ec6a37adf039

Co-authored-by: Jesper Schulz-Wedde <jesper.schulzwedde@microsoft.com>
This commit is contained in:
Jesper Schulz-Wedde 2026-07-14 14:20:27 +02:00 committed by GitHub
parent be1b92b624
commit 3d29c172a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 136 additions and 478 deletions

View file

@ -0,0 +1,23 @@
table 50120 "FieldError Default Bad"
{
fields
{
field(1; "No."; Code[20]) { }
field(2; "Discount %"; Decimal) { }
field(3; "Currency Code"; Code[10]) { }
}
procedure ValidateForRelease()
begin
// This re-tests a field and gives FieldError a fully formed sentence.
// The framework already prepends the caption and appends the value,
// so this renders as "Currency Code The Currency Code field must have
// a value. in ..." caption repeated, capital letter mid-sentence,
// stray trailing clause.
if "Currency Code" = '' then
FieldError("Currency Code", 'The Currency Code field must have a value.');
if "Discount %" > 100 then
FieldError("Discount %", 'The Discount % must not be greater than 100 percent.');
end;
}

View file

@ -0,0 +1,21 @@
table 50120 "FieldError Default Good"
{
fields
{
field(1; "No."; Code[20]) { }
field(2; "Discount %"; Decimal) { }
field(3; "Currency Code"; Code[10]) { }
}
procedure ValidateForRelease()
begin
// TestField checks this required-field condition and raises the error
// with caption and record context supplied by the framework.
TestField("Currency Code");
// Condition already evaluated: pass only a lowercase predicate so it
// reads as one sentence after the auto-inserted caption and value.
if "Discount %" > 100 then
FieldError("Discount %", 'cannot exceed 100');
end;
}

View file

@ -0,0 +1,18 @@
---
bc-version: [all]
domain: error-handling
keywords: [fielderror, testfield, error-message, field-caption, lowercase-convention, record-context, validation]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Rely On FieldError's Auto-Generated Context And Pass Only A Lowercase Predicate
## Description
`Rec.FieldError(FieldNo)` does not just print the text you give it. Business Central automatically prepends the field caption, appends the current field value (when non-blank), and suffixes the table name and primary-key values for record identification. The optional second argument is only the middle predicate of that sentence — e.g. `"must be unique"`, not a whole self-contained message. Misunderstanding this leads to messages that duplicate the caption and value or read as broken grammar, because the framework's surrounding text is built to join a lowercase fragment.
## Best Practice
For a plain required-field check, prefer `TestField`, which tests the condition and raises the error in one call. When the condition is non-trivial and has already been evaluated, call `FieldError(FieldNo)` with no message to get the localized default (`must have a value`, `is not valid`, etc.), or pass a short lowercase predicate such as `FieldError(FieldNo, 'must be a positive number')`. Start the custom text with a lowercase letter so it reads as one sentence with the auto-inserted caption, and use a field-number reference (or the field token) rather than a hard-coded field name so captions and translations stay correct. Let the framework supply the caption, value, table, and key context for you.
## Anti Pattern
Re-testing a condition you already evaluated, or passing a fully formed sentence like `'The Amount field must be positive.'` to `FieldError`. The result reads as `Amount The Amount field must be positive. in Gen. Journal Line ...` — capital letter mid-sentence, caption and value repeated, and a stray trailing clause. Reviewer signals: a `FieldError` argument that names the field, restates the current value, starts with a capital letter, or ends with a period. Each is a sign the author treated `FieldError` like `Error` instead of as a predicate slotted into framework-generated context.

View file

@ -0,0 +1,26 @@
table 50122 "FieldError vs TestField Bad"
{
fields
{
field(1; "No."; Code[20]) { }
field(2; "Posting Date"; Date) { }
field(3; "Amount"; Decimal) { }
}
procedure PostDocument()
begin
// FieldError performs no comparison and raises as soon as it is
// reached, so this "check" terminates PostDocument every time the
// Posting Date is never actually tested, and the amount rule below is
// dead code.
FieldError("Posting Date", 'must be filled in');
if IsAmountOutsideAllowedRange("Amount") then
Error('Amount is out of range.');
end;
local procedure IsAmountOutsideAllowedRange(Value: Decimal): Boolean
begin
exit((Value < 0) or (Value > 1000000));
end;
}

View file

@ -0,0 +1,27 @@
table 50122 "FieldError vs TestField Good"
{
fields
{
field(1; "No."; Code[20]) { }
field(2; "Posting Date"; Date) { }
field(3; "Amount"; Decimal) { }
}
procedure PostDocument()
begin
// TestField performs this simple presence check and raises only when
// the field is empty. Self-documenting prerequisite.
TestField("Posting Date");
// Business logic has already determined the value is invalid;
// FieldError raises a tailored, record-aware message with no
// condition of its own.
if IsAmountOutsideAllowedRange("Amount") then
FieldError("Amount", 'is outside the approved posting range');
end;
local procedure IsAmountOutsideAllowedRange(Value: Decimal): Boolean
begin
exit((Value < 0) or (Value > 1000000));
end;
}

View file

@ -0,0 +1,18 @@
---
bc-version: [all]
domain: error-handling
keywords: [fielderror, testfield, field-validation, onvalidate, error-message, mandatory-field, record-context]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Choose `TestField` For Conditional Checks And `FieldError` For Already-Failed Validation
## Description
`TestField` and `FieldError` look interchangeable but behave differently, and choosing the wrong one produces either dead code or a check that never fires. `TestField` performs the comparison itself and throws only when the field is empty or does not match the supplied value; `FieldError` performs no comparison and always raises an error the moment it is reached. Both attach the field caption and the record's primary-key context to the message automatically, which is why neither should be replaced by a hand-built `Error` call that interpolates the field name as a literal.
## Best Practice
Use `TestField` when the condition is a simple presence-or-equality check on a single field — mandatory-field gates and prerequisite checks at the top of a procedure read clearly and self-document intent. Use `FieldError` inside an `OnValidate` trigger or a validation procedure where surrounding business logic has already determined the value is invalid and you want a specific, custom message. Rely on the built-in field-and-record context both methods add rather than re-stating the field name in the text.
## Anti Pattern
Calling `FieldError` to "test" a field — placing it on a path that is reached unconditionally and expecting it to validate — terminates execution every time because `FieldError` never evaluates a condition. The inverse smell is reaching for `TestField` when the rule needs a tailored message, then bolting a vague generic string onto a check that cannot express the real business reason. A reviewer can spot the first by a `FieldError` that is not guarded by a preceding `if`, and the second by a `TestField` whose intent comment describes a condition more complex than presence or equality.