Part 4: (Slightly) More Elegant Error Handling in Business Central

Collectible Errors

In part 3 we had a look at the new platform feature, collectible errors. Long story short: it makes scenarios where you want to collect and display multiple errors together (e.g. checking journal lines) much easier to code and read, no messing around with if Codeunit.Run then, read the post if you’re interested.

The only thing that let the team down was the horrible user interface to display the errors that have been collected. Shame, but at least the API has been designed with extensibility in mind and we can handle it ourselves. I’m using the same example as in the docs (but with a little added finesse).

GetCollectedErrors

There is a new global method, GetCollectedErrors. This will return a list containing the ErrorInfo objects which have been collected in the current transaction. That gives us all the detail that we need to display the errors in a user-friendlier way.

Show Errors Codeunit

I’ve created a new codeunit to accept the list of errors and show the standard Error Messages page. Of course, you can do whatever you want with this. Create a new table and page to display the errors if you like. For now, the Error Messages page does everything that I need.

See the commit with the changes here if you prefer: https://github.com/jimmymcp/error-message-mgt/commit/89b421db69656b3e2717a0b307975320c3d5ddef

codeunit 50103 "Show Errors"
{
    SingleInstance = true;

    procedure ShowErrors(Errors: List of [ErrorInfo]; Context: Variant)
    var
        TempErrorMsg: Record "Error Message" temporary;
        DataTypeMgt: Codeunit "Data Type Management";
        ErrorInfo: ErrorInfo;
        RecRef: RecordRef;
    begin
        if Errors.Count() = 0 then
            exit;

        foreach ErrorInfo in Errors do begin
            TempErrorMsg.LogDetailedMessage(ErrorInfo.RecordId, ErrorInfo.FieldNo, TempErrorMsg."Message Type"::Error, ErrorInfo.Message, ErrorInfo.DetailedMessage, '');
            if DataTypeMgt.GetRecordRef(Context, RecRef) then
                TempErrorMsg.Validate("Context Record ID", RecRef.RecordId());
            TempErrorMsg.SetErrorCallStack(ErrorInfo.Callstack);
            TempErrorMsg.Modify();
        end;

        Page.Run(Page::"Error Messages", TempErrorMsg);
        Error('');
    end;
}

A couple of notes on the above:

  • There is a SetContext method on the error message record which I’m not using. For reasons I’m not clear about that method sets the context fields on a global variable in the Error Message table, not on the record instance itself
  • Calling the Log methods on the Error Messages table captures the current callstack*, I’m overwriting that with the callstack on the Error Info object
  • The Error(”); on the final line ensures that code execution is stopped and the transaction rolled back. An error like this (outside of If Codeunit.Run then) cannot be collected

I’ve added the last line to the CheckLines method (the method which is collecting the errors):

[ErrorBehavior(ErrorBehavior::Collect)]
local procedure CheckLines(VideoCallBatch: Record "Video Call Batch")
var
    VideoJnlLine: Record "Video Journal Line";
    ShowErrors: Codeunit "Show Errors";
begin
    //check lines code
    ShowErrors.ShowErrors(GetCollectedErrors(), VideoCallBatch.RecordId());
end;

The result is something like this. The Error Messages page does the rest, drilling down to show the callstack, adding an action to open the related record, displaying the name of the field name related to the error.

Error Messages page

Check the docs for more info. The error info API includes IsCollectingErrors(), HasCollectedErrors() and ClearCollectedErrors().

*incidentally, with an ugly hack like this (https://tecman.typepad.com/software_answers/2016/07/context-of-events-in-microsoft-dynamics-nav.html) 😉 I’m not sure whether to be pleased that I wasn’t missing a better way to do it back then or disappointed that there still isn’t a better way to do it 6 years later.**

**Actually, maybe this could be improved with a collectible error?***

leonardo dicaprio dreaming GIF
***thinking about that for too long…

Part 3: (Slightly) More Elegant Error Handling in Business Central

Intro

This is a continuation of a series of posts started around a year ago – you can find the old posts here if you are interested.

Briefly, the goal is this. I’m posting a journal and there are several checks that I need to perform on each line before they are posted. Instead of stopping and showing the first error I want to validate all of the lines and show all of the errors to the user at once.

Previously

The previous posts have been about using the Error Message Management codeunit to achieve that. It works, but I found it clunky. You need to avoid throwing an actual error, but instead collect the problems into a temporary table to display at the end. Then an actual error message is thrown to prevent the journal from being posted.

Problems

What are the problems with that approach?

First, not throwing an error if often harder than throwing one. Instead of simple calls to TestField or Error you need to make corresponding calls to ErrorMessageMgt.LogTestField and ErrorMessageMgt.LogError. In itself that’s OK, but what my be more of a problem is wrapping the posting routine with if Codeunit.Run() then

if not Codeunit.Run(Codeunit::"Posting Routine") then
  ErrorMessageHandler.ShowErrors();

It depends on the context in which you are calling the posting routine. If you are calling it from a page action it’s probably fine. If you are calling it in the middle of some other business logic, not so much.

Second, testing the routine is a bit of a pain. If you want to write an automated test that asserts that when you post the journal line without a posting date then an error is thrown, you can’t. At least not in the way that you expect.

asserterror JournalLine.Post();
Assert.ExpectedError('Posting Date must not be blank');

Something like this won’t work – because an actual TestField error is never thrown. That error message is collected in the temporary table. The only actual message that is thrown is a blank one by the Error Message Handler codeunit. So then you have to either:

  1. complicate your tests by collecting the error messages and asserting that they have the value you expect
  2. complicate your production code with an option to directly throw the errors rather than log them e.g. make sure that you don’t have an active instance of Error Message Management. It isn’t difficult, it just feels messy

Collectible Errors

From BC19 we have the concept of collectible errors. This is quite a different – and better, I think – approach to the same problem. Instead of a framework in AL which we need to dance around avoiding calling Error it is a platform feature which allows us to call Error but tell the platform that the error is collectible and that code can continue to be executed.

The method that the errors are thrown in must indicate that it allows errors to be collected with a new attribute ErrorBehaviour.

[ErrorBehaviour(ErrorBehaviour::Collect)]
local procedure CheckLine(var JournalLine: Record "Journal Line")
begin
  Error(ErrorInfo.Create('Some error message', true));
end;

The Error method has a new overload which takes an ErrorInfo type instead of some text. The ErrorInfo type indicates whether it can be collected or not.

If both the ErrorInfo and the method in which it was thrown are set to allow collection then the error message will not be immediately shown to the user and the code will continue to execute.

Show me the code…

Changes to the Video Journal Batch record

You can view the full changes in this commit: https://github.com/jimmymcp/error-message-mgt/commit/5faa82e614c13017c687d2255305675df0049b29

This is the Post method which is called from the page. You can see the benefits immediately. We can get rid of all that nonsense activating the error message framework and calling if Codeunit.Run. Just call the posting routine and let it do its thing. Much easier to follow.

Next, in the codeunit that handles the batch posting we can get rid of the calls to Error Message Management. I’ve added a new CheckLines method and decorated it with the ErrorBehaviour attribute. This tells the system that any collectible errors which occur within the scope of this method can be collected and code execution can continue.

The level at which we set the ErrorBehaviour attribute is important. I want to continue to check all journal lines in the batch and then stop and show any errors which have been collected. That’s why the ErrorBehaviour is set here – at the journal batch level – rather than at journal line level.

When the system finishes executing the code in this method it will automatically check whether any errors have been collected and show an error message if they have.

Finally, these are the changes to the codeunit which actually checks the journal line. Again, we can ditch the references to the Error Message Management codeunit and replace them with straightforward calls to Error or TestField.

Rather than passing some text with the error message we can pass an ErrorInfo type, returned by ErrorInfo.Create. This is the signature. At a minimum pass the error text, but we also want to indicate that this error can be collected via the collectible parameter. I’m including the instance of Video Journal Line and field number where appropriate as well.

Great to see that TestField has an overload which accepts an ErrorInfo object. The system will fill in the usual error text for you, “<field number> must have a value in <record id>”.

The other parameters are interesting, maybe more about those another time.

procedure Create(Message: Text, [Collectible: Boolean], var [Record: Table], [FieldNo: Integer], [PageNo: Integer], [ControlName: Text], [Verbosity: Verbosity], [DataClassification: DataClassification], [CustomDimensions: Dictionary of [Text, Text]]): ErrorInfo

How does it look?

Put that all together and attempt to post a couple of journal lines which have some validation errors. How does it look?

You get an error dialog as usual. Only this time it says that “Multiple errors occurred during the operation” and gives you the text of the first error message. Click on Detailed information to see a list of all the errors that were collected.

This is what you get.

Kind of underwhelming right?

It was all going so well up to this point but I’ve got a few issues with this:

  1. Given that I’ve gone to the trouble of collecting multiple errors to show to the user all at once it seems counter-intuitive to make the user expand the error to see all the details
  2. Is it just me or is this not easy to read? Once an error message breaks two lines it isn’t obvious how many errors there are. You can’t expand the dialog horizontally either. Even with relatively few errors I’ve had to scroll down to be able to read them all
  3. TestField errors include the record id, which is fine, but for the custom errors I’ve gone to the trouble of giving the record and field number that contains the problem…but that isn’t shown anywhere. I’ve only got 2 lines in my journal in this case, but if I had tens or hundreds it would be really difficult to match the validation errors to the lines that caused them

Custom Handling of Errors

There is a way that we can handle the UI of the error messages ourselves, which is great – and I’ll show an example of that next time. Kudos to the platform team for building that capability in from the start, it’s just a shame that it’s necessary. Call me picky, but I don’t think the standard dialog is really useable.

BC19 CU0

By the way, this doesn’t work properly in BC19 CU0. You have to set the target in app.json to OnPrem – which shouldn’t be necessary. That’s been fixed now.

Part 2a: (Slightly) More Elegant Error Handling in Business Central

One of the underrated advantages to doing a little blogging is that you can write about a subject you know a little about and have people who actually know what they are talking about reply to tell you a better way to do it.

There’s probably a minimum threshold of credibility on the subject you need in order for people to post serious replies. If I posted some nonsense about my keyhole surgery technique I doubt I’d get helpful corrections from the Royal College of Surgeons. It would also represent something of a departure from my usual DevOps, Git, testing and BC development posts.

Anyway, I had some useful comments on my previous post about error handling – thanks.

Use the Error Message Table

Henrik Helgesen pointed out you can skip all the codeunits, activation, context, finishing… and just use the Error Message table directly. It has some a bunch of LogXYZ methods for recording errors or messages.

It has a method to determine if there are error messages to show and a method to show them. Nice and simple and, for the scenario that I outlined, probably more appropriate.

LogTestField()

In the last post I complained about the lack of TestField functionality in the Error Message Mgt. codeunit. Kilian replied to say that the method exists in BC17. I doubt that has anything to do with my post – but I’m happy to take some credit if required. It has the signature that you’d expect.

procedure LogTestField(SourceVariant: Variant; SourceFieldNo: Integer) IsLogged: Boolean

That makes the error handling code far less verbose and, crucially, we don’t have to provide a label or any translations for the error message – that’s handled by the framework.

Consistent Behaviour With Base App

LogTestField in the base app

How useful is it for partners to invest in this sort of error handling if the base app doesn’t use it? A consistent user experience might still be better than improving our error handling but departing from standard paradigms in the process. Fair point.

Although actually, it looks like more of this is coming to the base app. This is a snip of the results when searching for “LogTestField” in the base app in BC 17.1.17104.0-W1.

49 results in 2 files. OK, so probably still a long way to go to make this the default user experience but its a start. I’m hopeful that this is an area that Microsoft will pay some attention to over the next few versions, improve the framework and make it easier for us to follow their lead.

Part 2: (Slightly) More Elegant Error Handling in Business Central

Part 2 of the series that I said that I was going to write has been a long time coming. If you don’t know what I’m talking about you might want to read the first post in the series here. Unfortunately it is possible that this series reflects the functionality that it is describing: full of early promise, but on closer inspection a little convoluted and disappointing. I’ll leave you to be the judge of that.

Unfortunately, I’ve just found the framework a little annoying to work with. Maybe I’m missing the correct way to use it but it seems like there are too many objects involved without providing the functions that I was expecting. Then again, if I am missing the best way to use it then that illustrates my point – it’s just not very friendly to work with. I’ll try to make some constructive suggestions as we go.

Scenario

A quick reminder of what we’re trying to achieve here. I’ve got a journal page to record all the video calls for work and family that I’m having.* Before the journal is posted the lines are checked for lots of potential errors. Rather than presenting the user with one error at a time we are trying to batch them all together and present them in a list to be resolved all at once. I’m using the error message handling framework to do it.

*to be clear, I’m not using it. I’m sad…but not that sad**

Overview

Some basic principles to bear in mind when dealing with the Error Message Mgt. codeunit

  1. You need to trap all the errors
    • The framework provides a way of collecting messages and displaying them to the user in a list page. It doesn’t fundamentally change how error handling in BC works. If you encounter an un-trapped error the code execution will stop, transaction be rolled back etc.
    • Obviously that includes TestField() and FieldError() calls, not just Error()
  2. The Error Message framework must be activated before calling the code that you want to trap errors for
  3. Call PushContext to set the current context for which you are handling errors
  4. Call Finish to indicate that the previous context is complete
  5. You need to determine whether there are any errors to display and then, if so, display them

Example

This is where the posting of my journal batch begins. We need to activate the error handling framework and if an error is trapped in the posting codeunit then show the errors that have been collected.

There is a ShowErrors method in the Error Message Management codeunit, but its only for on-prem. Don’t ask, I don’t know. You need to use if Codeunit.Run (or a TryFunction I suppose – although don’t) to determine whether to there are any errors to show. There is a HasErrors method in the Error Message Handler codeunit but that’s also only for on-prem. Still don’t ask.

procedure Post()
var
    VideoCallBatchPost: Codeunit "Video Call Post Batch";
    ErrorMessageMgt: Codeunit "Error Message Management";
    ErrorMessageHandler: Codeunit "Error Message Handler";
begin
    ErrorMessageMgt.Activate(ErrorMessageHandler);
    if not VideoCallBatchPost.Run(Rec) then
        ErrorMessageHandler.ShowErrors();
end;

It would have been nice if there was a way to do with without declaring an extra two codeunits – but I don’t think there is.

Onto the next level on the callstack. Call PushContext with a record that gives the context within which the errors are being collected. Run the code that we want to collect errors from and then Finish. If any errors have been encountered the Finish method will throw an error with a blank error message to ensure that the transaction is rolled back to the last commit.

If Finish is called when GuiAllowed is false then SendTraceTag is called to “send a trace tag to the telemetry service”. Interesting.

local procedure PostBatch(VideoCallBatch: Record "Video Call Batch")
var
    VideoJnlLine: Record "Video Journal Line";
    ErrorMessageMgt: Codeunit "Error Message Management";
    ErrorContextElement: Codeunit "Error Context Element";
begin
    ErrorMessageMgt.PushContext(ErrorContextElement, VideoCallBatch, 0, '');
    VideoJnlLine.SetRange("Batch Name", VideoCallBatch.Name);
    VideoJnlLine.FindSet();
    repeat
       VideoJnlLine.Post();
    until VideoJnlLine.Next() = 0;

    ErrorMessageMgt.Finish(VideoCallBatch);
end;

Now into the journal line posting and all the checks that are performed on each line. I won’t copy out the entire function – it’d be a bit tedious and you can check the source code afterwards if you’re interested.

local procedure Check(var VideoJnlLine: Record "Video Journal Line")
var
    GLSetup: Record "General Ledger Setup";
    ErrorMessageMgt: Codeunit "Error Message Management";
begin
    if VideoJnlLine."No. of Participants" = 0 then
        ErrorMessageMgt.LogErrorMessage(VideoJnlLine.FieldNo("No. of Participants"), StrSubstNo('%1 must not be 0', VideoJnlLine.FieldCaption("No. of Participants")), VideoJnlLine, VideoJnlLine.FieldNo("No. of Participants"), '');

    if VideoJnlLine."Duration (mins)" = 0 then
        ErrorMessageMgt.LogErrorMessage(VideoJnlLine.FieldNo("Duration (mins)"), StrSubstNo('%1 must not be 0', VideoJnlLine.FieldCaption("Duration (mins)")), VideoJnlLine, VideoJnlLine.FieldNo("Duration (mins)"), '');

    if VideoJnlLine."Posting Date" = 0D then
        ErrorMessageMgt.LogErrorMessage(VideoJnlLine.FieldNo("Posting Date"), StrSubstNo('%1 must be set', VideoJnlLine.FieldCaption("Posting Date")), VideoJnlLine, VideoJnlLine.FieldNo("Posting Date"), '');

This is where it really starts to get a bit messy. The TestFields are gone, replaced with calls to LogErrorMessage. LogError and LogSimpleErrorMessage are alternatives with slightly different signatures. Pass in the field no, error message, record and “help article code” related to the error and they will be collected by the framework.

If any errors have been logged then the Finish function (see above) will throw an (untrapped) error and prevent the journal from actually being posted.

Conclusion

I really tried to enjoy working with this. I’d like to have better error handling in our apps – but I don’t think we’re going to get round to introducing this sort of thing any time soon. The parameters on this method are too clunky.

  • It requires a “context” field no. and a “source” field no. – I’m still not clear what the difference is
  • I have to provide the error message text. That’s a problem. With TestField I can leave the system to generate the correct error text, in whatever language the client is set to. This way I have to create a label (I didn’t in my example because I’m lazy) and then translate it into different languages
  • I don’t know what I’m supposed to provide for “help article code”
  • I was hoping for an ErrorMessageMgt.TestField method. Couldn’t I just pass in my record and the field no. that I’m testing? I want to leave the framework to determine if an error needs to be logged and, if so, the correct text

You can view the changes that I’ve made since the first blog post here: https://github.com/jimmymcp/error-message-mgt/commit/6d768c5552c0fad433e75dea15a7d1d064cb040c

I’d love someone to tell me that I’ve missed how easy this framework is to work with and they’ve had a great time with it. It looked like it was going to be great but left me a bit flat. Like a roast potato that you’ve saved for your last mouthful at Sunday lunchtime only to discover it’s actually a parsnip.

(Slightly) More Elegant Error Handling in Business Central

This is an intro post to the Error Message Mgt. codeunit and related objects. NAV has never brilliant when it comes to error handling, for a couple of reasons.

  1. The error messages themselves sometimes leave a lot to be desired
  2. The whac-a-mole nature of fixing multiple errors by finding one at a time and attempting to post/register again

There isn’t a lot we can do about the standard error messages, but we can write more considerate errors for our users. Describe the problem and guide the user to the solution as much as possible i.e. not “There was nothing to handle”.

What about #2? What if we could line up all the moles so that the user could whack them all in one go?1 We can use the Error Message Management objects to do just that.

This is a useful post you could take a look at – http://www.mynavblog.com/2019/04/09/how-to-write-error-and-confirm/ – but even having read that I still struggled my way through how to use it and write tests around it. I didn’t find the framework particularly easy or intuitive to work with so hope I can save someone else some head-scratching.

1 metaphorical moles. I’m not endorsing whacking actual moles.

Scenario

You might want to use this framework when you’ve got some process that could throw errors for multiple different reasons. Usually these are going to be some posting or registering routine for a journal line or a document.

Often there are all sorts of things that can wrong with those routines – posting date ranges, dimension errors, mandatory fields that have not been populated, missing posting setup, missing no. series… blah blah blah.

Rather than just throwing an error for the first problem we encounter we want to collect them together so that the user can fix them before posting again.

I was going to attempt a single overview post, but I’ve decided against that. I think it will be more useful (hopefully) to work through an example – albeit a silly one – in stages. I’ve got a small app for posting a record of video calls – because that’s what we need right now, more video calls and more admin.

The app adds a journal page to record the platform (Teams, Zoom, WhatsApp or Skype) the type of call, date, duration and participants. Before the journal can be posted there is a deliberately convoluted process to check for various errors. Concisely summarised below:

  1. No. of Participants must not be 0
  2. Duration (mins) must not be 0
  3. Posting Date must not be blank
  4. Posting Date must be within allowed posting dates for the user
  5. Zoom calls cannot be over 40 minutes when the No. of Participants is > 2 – I’m a cheapskate and have got a free account
  6. Teams cannot be used for a call type of Family Quiz – surely no family is that corporate?
  7. WhatsApp should not be used for groups of more than 4 – it’s bad enough with 2
  8. WhatsApp should not be used for a call type of Customer Demo – I mean, you don’t…do you?
  9. Skype isn’t used for more than 2 participants – I know technically it can be…it just isn’t
  10. Family quizzes can’t be held Monday – Thursday
  11. A call type of Daily Team Call cannot be more than 45 minutes long – you need to find a smaller team to have daily calls with
  12. A call type of Daily Team Call cannot have more than 30 participants – see #11

While this is daft, it is an example of how a journal might not be able to be posted for lots of different reasons. Normally we expect the user to fix those errors one at a time and, if they’ve still got the will to live, post the batch when they have resolved them all.

At the moment I’m just using Testfield and Error to throw errors when a journal line is valid. Over the next few posts I’m going to see if I can the Error Message Mgt objects to build a list of errors and display them all at once. Then we’re going to talk about how to test this behaviour.

The source code is here: https://github.com/jimmymcp/error-message-mgt Disclaimer: it sucks. This is an example of using the error handling tools, not of how to write a good journal.

Sales Header Posting

In the meantime, if you want to see an example of this sort of error collection in the base application then look at SendToPosting on the Sales Header table.

ErrorMessageMgt.Activate(ErrorMessageHandler);
ErrorMessageMgt.PushContext(ErrorContextElement, RecordId, 0, '');
IsSuccess := CODEUNIT.Run(PostingCodeunitID, Rec);
if not IsSuccess then
  ErrorMessageHandler.ShowErrors;

Then in the Sales-Post codeunit you’ll see this kind of thing (this from CheckAndUpdate)

ErrorMessageMgt.PushContext(ErrorContextElement, RecordId, 0, CheckSalesHeaderMsg);
CheckMandatoryHeaderFields(SalesHeader);
if GenJnlCheckLine.IsDateNotAllowed("Posting Date", SetupRecID) then
  ErrorMessageMgt.LogContextFieldError(...);