TryFunctions are expensive!

Recently, I did a small change to one of my apps, and suddenly everything came to a crawl. Turned out it was the usage of TryFunctions that was the root cause. Check out the video for the details:

https://youtu.be/xoTQnxvESH4

In this video, Erik demonstrates a performance pitfall that many AL developers might not be aware of: TryFunctions in Business Central are expensive — not in monetary cost, but in execution time. When used in tight loops, especially when the TryFunction frequently fails, they can turn a process that takes seconds into one that takes minutes or even hours. Erik walks through a concrete example using JSON value type checking and shows how to refactor the code for dramatically better performance.

The Problem: Determining a Value’s Type

Erik sets the scene with a real-world scenario he encountered while working on his SQL app. He needed to export data to Excel and wanted to format cells correctly — meaning he needed to determine whether a given JsonValue was a date, a time, a decimal, or just text.

The JsonValue type in AL has methods like .AsDate(), but if the value isn’t actually a date, calling .AsDate() throws a runtime error:

“Unable to convert from Microsoft.Dynamics.Nav.Runtime.NavJsonValue to Microsoft.Dynamics.Nav.Runtime.NavDate.”

(As Erik notes with a laugh — we can never escape the “Nav” in the runtime!)

The Gut Reaction: Use a TryFunction

The natural instinct for most AL developers is to wrap the conversion in a TryFunction. A TryFunction effectively returns a boolean — true if the code succeeds, false if an error occurs — allowing you to handle the failure gracefully without surfacing an error to the user.

[TryFunction]
local procedure ValueAsDate(v: JsonValue; var OutDate: Date)
begin
    OutDate := v.AsDate();
end;

You’d then call it like this:

if ValueAsDate(value, d) then
    // It's a date — use d
else
    // Not a date

This works perfectly from a functional standpoint. But there’s a hidden cost.

A Quick Note on Debugging

Before diving into the demo, Erik highlights an important Visual Studio Code setting. If you have “Break on Error” set to “All” in your launch configuration, the debugger will break on every TryFunction failure, making development painful. Make sure to set it to “Exclude Try” so the debugger ignores errors caught by TryFunctions.

Benchmarking the TryFunction Approach

Erik sets up a simple benchmark: loop 10,000 times, calling the TryFunction each iteration, and measure the elapsed time:

trigger OnOpenPage();
var
    Value: JsonValue;
    d: Date;
    i: Integer;
    Start: DateTime;
begin
    Value.SetValue('ABC'); // Not a date — TryFunction will fail every time

    Start := CurrentDateTime();
    for i := 1 to 10000 do begin
        if ValueAsDate(value, d) then;
    end;
    message('Elapsed %1', CurrentDateTime() - Start);
end;

The result? Four seconds for 10,000 iterations — with each iteration failing and throwing an internal exception.

The Alternative: Use Evaluate

Erik then creates an alternative version that avoids the TryFunction entirely by leveraging AL’s built-in Evaluate function. Evaluate is remarkably clever — it can parse various formats and, crucially, it returns a boolean indicating success or failure without throwing an exception:

local procedure ValueAsDate2(v: JsonValue; var OutDate: Date): Boolean
begin
    exit(Evaluate(OutDate, v.AsText(), 9));
end;

The magic number 9 passed as the third parameter tells Evaluate to only accept XML format dates, which happens to be the format that JSON typically uses for date values. This makes it a perfect fit for this use case.

Running the same 10,000-iteration benchmark with this approach: 7 milliseconds.

The Results Compared

Here’s a summary of the benchmark results Erik observed:

  • TryFunction (value is NOT a date — exception every time): ~4,000 ms (4 seconds)
  • Evaluate-based approach (value is NOT a date): ~7 ms
  • TryFunction (value IS a date — no exception): ~11 ms
  • Evaluate-based approach (value IS a date): ~9 ms

When the TryFunction succeeds (no exception thrown), the performance is comparable. The catastrophic slowdown only occurs when the TryFunction fails — which is precisely the scenario where you’d rely on the TryFunction pattern in the first place.

That’s roughly a 500x performance difference in the failure case.

The Complete Source Code

Here’s the full example Erik built during the video:

namespace DefaultPublisher.TryFunctionsAreExpensive;

using Microsoft.Sales.Customer;

pageextension 58400 CustomerListExt extends "Customer List"
{
    trigger OnOpenPage();
    var
        Value: JsonValue;
        d: Date;
        i: Integer;
        Start: DateTime;
    begin
        Value.SetValue(today());

        Start := CurrentDateTime();
        for i := 1 to 10000 do begin
            if ValueAsDate(value, d) then;
        end;
        message('Elapsed %1', CurrentDateTime() - Start);
    end;

    local procedure ValueAsDate2(v: JsonValue; var OutDate: Date): Boolean
    begin
        exit(Evaluate(OutDate, v.AsText(), 9));
    end;

    [TryFunction]
    local procedure ValueAsDate(v: JsonValue; var OutDate: Date)
    begin
        OutDate := v.AsDate();
    end;
}

To reproduce the different benchmarks, swap between calling ValueAsDate (TryFunction) and ValueAsDate2 (Evaluate-based) in the loop, and change Value.SetValue(today()) to Value.SetValue('ABC') to test the failure case.

Why Are TryFunctions So Expensive?

Erik provides a helpful technical explanation. When you write AL code, Microsoft doesn’t compile it directly. Instead, AL code is transpiled into C#, which is then compiled by the C# compiler into IL (Intermediate Language), and finally JIT-compiled to machine code at runtime.

TryFunctions use .NET exceptions under the hood. .NET exceptions are notoriously expensive even in pure C# — they involve stack unwinding, capturing stack traces, and other overhead. But it appears that AL’s TryFunction mechanism adds even more overhead on top of the native .NET exception cost, possibly due to additional state management, transaction handling, or logging within the Business Central runtime.

Practical Guidance

Based on Erik’s findings, here are the key takeaways:

  1. Avoid TryFunctions in tight loops — If you’re processing thousands or millions of records, the cumulative cost of failed TryFunctions is devastating.
  2. Consider the failure rate — If 99% of calls succeed and only 1% fail, the performance impact may be acceptable. But if failures are the common case (as in type-checking scenarios), find an alternative.
  3. Use Evaluate for type checking — The Evaluate function with its boolean return is purpose-built for “can this value be converted?” scenarios and is orders of magnitude faster than a TryFunction.
  4. Think before you try — In Erik’s real-world case, he had three TryFunctions per cell (checking date, time, and decimal), and for text values all three would fail. This turned an instant export into a two-minute ordeal.

Conclusion

TryFunctions are a useful tool in AL, but they come with a significant hidden performance cost — especially when they fail. In Erik’s demonstration, the difference between a TryFunction-based approach and an Evaluate-based approach was roughly 500x for the failure case (4 seconds vs. 7 milliseconds over 10,000 iterations). In real-world scenarios processing large datasets, this can mean the difference between a process completing in seconds versus taking hours. The lesson is clear: always look for non-exception-based alternatives before reaching for a TryFunction, particularly in performance-sensitive code paths.