Preconditions and TryFunctions in AL (Business Central)
Preconditions and TryFunctions in AL
TryFunctions in AL deservers more attention. They can play an important role in making your code more safe, robust and easier to read.
A little bit of Theory and Terminology
A function f maps a domain A to its codomain B.
\[f: A \rightarrow B\]The function f taks an argument a and returns a value b.
\[f(a) = b\]This article is about ensuring that the argument a is within the domain A of the function f before running the function. This is a precondition for running the function.
Automated Testing is about ensuring that the value b corresponds with the expected mapping into the codomain B.
Checking Preconditions
Before looking at the TryFunction, let us look at a simple case of ensuring that the preconditions for running the function are met.
We have been asked to make an average function given a total and a number of lines.
That is easy, we think and write the following.
procedure Average(Total: Decimal; NumberOfLines: Integer): Decimal
begin
exit(Total / NumberOfLines);
end;
This is an example of code that is not robust.
Except that we are not checking the preconditions. The preconditions for this function is that the NumberOfLines is a positive number. I often write the precondition check in the following format in the start of the function:
if not (Precondition) then
exit();
Like this:
procedure Average(Total: Decimal; NumberOfLines: Integer): Decimal
begin
if not (NumberOfLines > 0) then
exit(0);
exit(Total / NumberOfLines);
end;
But if a developer calls this function with a negative number of lines, something is wrong. So I want to insist harder on the valid domain for the function, and I throw an error.
procedure Average(Total: Decimal; NumberOfLines: Integer): Decimal
var
ErrorLbl: Label 'Number of Lines cannot be a negative number.';
begin
if (NumberOfLines < 0) then
Error(ErrorLbl);
if (NumberOfLines = 0) then
exit(0);
exit(Total / NumberOfLines);
end;
I could go further and check that the Total should be zero if the NumberOfLines is zero. And I could avoid the calculation and possible rounding errors when the NumberOfLines is one.
procedure Average(Total: Decimal; NumberOfLines: Integer): Decimal
begin
if (NumberOfLines < 0) then
Error('Number of lines cannot be a negative number.');
if (NumberOfLines = 0) then begin
if not (Total = 0) then
Error('The total must be zero, when the number of lines is zero.');
exit(0);
end;
if (NumberOfLines = 1) then
exit(Total)
else
exit(Total / NumberOfLines);
end;
This is an example of code that is robust.
TryFunctions in AL
Now let us consider a standard system function in AL: System.DMY2Date
Date := System.DMY2Date(Day: Integer, Month: Integer, Year: Integer);
The domain is
- Day: Integer between 1 and 31
- Month: Integer between 1 and 12
- Year: Integer between 0 and 9999
But checking that each argument is within its domain is not enough to prevent the function from failing hard during runtime.
Date := System.DMY2Date(29, 2, 2019);
February 29th, 2019 is not a valid date.
Imagine that you are getting text data from an import file or an API, and you have to convert it into a date.
To protect our code, we add the TryFunction Attribute to our function. The TryFunction cannot have a return value, since it default returns a boolean.
So we add var
to return the calculated value NewDate
as an argument.
[TryFunction]
procedure TryDmy2Date(Day: Integer; Month: Integer; Year: Integer; var NewDate: Date)
begin
NewDate := DMY2Date(Day, Month, Year);
end;
This leads us to answer a very important question. What should we do if the function fails?
For this, I develop helper functions like these.
procedure IsDmyValidDate(Day: Integer; Month: Integer; Year: Integer): Boolean
var
NewDate: Date;
begin
exit(TryDmy2Date(Day, Month, Year, NewDate))
end;
procedure Dmy2DateWithDefault(Day: Integer; Month: Integer; Year: Integer; DefaultDate: Date) NewDate: Date
begin
if not TryDmy2Date(Day, Month, Year, NewDate) then
NewDate := DefaultDate;
end;
The IsDmyValidDate
function allows the developer to check if the information is valid or not.
The Dmy2DateWithDefault
function forces the developer to decide what date should be returned in case of failure. Maybe the default value is the 0D
, or maybe Today()
or something else.
Explained by Erik Hougaard
Yesterday (January 29, 2024) Erik posted this video on YouTube: Is your AL code robust or weak, what kind of developer are you with Business Central?
Erik walks you through the Microsoft Learn: Failure modeling and robust coding practices page.
Checking your preconditions is a good example of “Don’t trust consumers of your code” and my TryFunction example speaks for itself.
Clean AL Code Initiative
- Part 1: Rulesets in Business Central
- Part 2: Namespaces in AL
- Part 3: VS Code Extensions for AL
- Part 4: Automated Tests in AL
- Part 5: Advanced CodeCop Analyzer and Custom Rulesets
- Part 6: How to make a code review
- Part 7: Preconditions and TryFunctions in AL
Elm Language and Maybe
This approach is inspired by the Elm Language. Elm is a delightful language for reliable web applications.
Elm can add Maybe to a domain type like Float
.
The Maybe
type is defined as
type Maybe a
= Just a
| Nothing
Example:
Convert a String to a Float.
> String.toFloat
<function> : String -> Maybe Float
> String.toFloat "3.1415"
Just 3.1415 : Maybe Float
> String.toFloat "abc"
Nothing : Maybe Float
The Dmy2DateWithDefault
function above is inspired by the Maybe with Default.
We do not have a Maybe type in AL, but we have TryFunctions to keep us safe.