Not all code paths return a value

Properties
PC0038 Info Usage Code Fix Ignore Obsolete

Procedures with a return type must produce a value on every reachable path. If one branch falls through without returning a value, AL can end up returning an implicit default value (0, false, empty text), which hides logic bugs and makes behavior harder to reason about.

This rule flags declarations where not all reachable code paths return a value. A path ending with a built-in error call does not return to the procedure and therefore does not need a return value.

Example

The following procedure misses a return value when the condition is false:

procedure GetAmount(IncludeVat: Boolean): Decimal
begin
    if IncludeVat then
        exit(AmountInclVat);
end;

Return a value on all paths:

procedure GetAmount(IncludeVat: Boolean): Decimal
begin
    if IncludeVat then
        exit(AmountInclVat)
    else
        exit(AmountExclVat);
end;

When the diagnostic is reported

The rule is reported when all of the following are true:

  • The declaration is a procedure with an explicit return type
  • At least one reachable path can end without returning a value
  • The declaration is not marked with TryFunction

Triggers are intentionally excluded from this rule, even when they declare a return type.

For named return variables, an assignment must be guaranteed on every path that reaches the end of the procedure. Assignments made through a var parameter or by using the return variable as a call receiver count, because those calls can write the value. The rule also follows calls in conditions, case selectors, loop conditions, for bounds, and foreach collections when the expression must execute. It remains conservative for the right side of and and or, and for either branch of a conditional expression, because those expressions can be skipped.

A case statement without else is accepted when it handles every enum or option value visible to the current compilation. Values added by an extension available at compilation time are included in that check.

Exception

Paths that end with a built-in call that never returns are accepted: Error(...), Record.FieldError(...), and FieldRef.FieldError(...).

procedure GetAmount(IncludeVat: Boolean): Decimal
begin
    if IncludeVat then
        exit(AmountInclVat)
    else
        Error('Prices without VAT are not supported.');
end;

procedure GetUnitPrice(Item: Record Item): Decimal
begin
    if Item."Unit Price" > 0 then
        exit(Item."Unit Price")
    else
        Item.FieldError("Unit Price", 'must be positive.');
end;

Only these built-in methods qualify. A user-defined procedure named Error or FieldError is a normal call and does not terminate a path.

Known limitation: Error(ErrorInfo) is currently treated as terminating even when ErrorInfo.Collectible is true and the call runs in an ErrorBehavior::Collect scope. In that situation AL can continue after the call, but determining it requires data-flow and enclosing-call analysis beyond this rule’s current invocation classification.

A named return variable passed to a var parameter, or used as the receiver of a call, counts as assigned:

procedure BuildGreeting() Greeting: Text
begin
    GetGreeting(Greeting);
end;

local procedure GetGreeting(var GreetingText: Text)
begin
    GreetingText := 'Hello';
end;

procedure FindCustomer(CustomerNo: Code[20]) Customer: Record Customer
begin
    Customer.Get(CustomerNo);
end;

Passing it by value (without var) does not count.

See also