TRY.. CATCH in C/AL

  • Post category:Thoughts
  • Post comments:23 Comments
  • Reading time:6 mins read

Did I CATCH your attention yet? At least I did TRY.

Now that I have completed my series on exception handling in C/AL, a very valid question pops up: why don’t we have try..catch syntactical constructs in C/AL, the way we have it in other programming languages?

If there was a top list of C/AL features that people could vote, no doubt this would win without much competition. Wouldn’t it just be an insanely useful C/AL feature if you could write code such as this:

image

Or something along these lines… Yes, it would be just beautiful beyond comprehension.

And now let me ask you a question (I know the answer already, I just want you to ask yourself this question): Do you know when we are going to have this feature in C/AL?

Never.

And now let me make a heretical statement: it’s a good thing that it will never be a part of C/AL.

(Well, of course, I’d love it if I were ever proven wrong by Microsoft, oh I’d just love it so much. But unfortunately, I am pretty darned sure I am right on this one. And let me explain why I believe so.)

It all has to do with database and how C/AL handles write transactions. In C/AL you have no means of controlling transactions – they are always implicitly started when the first database write statement is executed, and it automatically commits when the code execution completes, or when you explicitly call the COMMIT function. However, the problem is – there is no ROLLBACK function in C/AL – the transaction is rolled back implicitly whenever there is any kind of error.

Take a look at this piece of code:

image

We start by creating a new customer, and then we call a codeunit. If there is an error inside this codeunit , the whole transaction is rolled back (including this customer we just created). So far, so good, and simple.

However, what will happen if you do this:

image

Exactly. The mother of all ugly errors will happen:

image

“The following C/AL functions are limited during write transactions because one or more tables will be locked… Codeunit.Run is allowed in write transactions only if the return value is not used.”

Now, why is that? Why exactly does this error happen? What exactly is so different here, than it was with the first code example?

This is what happens: when the first line of code executes, a transaction is implicitly started. Now, the IF CODEUNIT.RUN construct says that if the codeunit execution fails, your code continues executing. However, what happens to the database changes? Well, we already know, when an error happens, the database changes are rolled back. The problem is, which database changes? Only those that happened inside the codeunit, or also any changes that happened before you entered the codeunit? To avoid guessing, NAV fails with this wordy error message and asks you to restructure the code.

Now imagine we had the TRY..CATCH block in C/AL. What if there are five successful database write operations in the TRY block, and then an error is encountered? Should these five operations be committed, or should they be rolled back?

You see, try..catch blocks are easy if we have no transactions. But once transactions enter the playground, suddenly you have a valid question about what should happen uncommitted changes if an error is successfully trapped.

Of course there is a solution to that: being able to control the transactions directly. Just like in T-SQL. T-SQL has TRY..CATCH blocks, but they don’t handle anything directly, they still require you to manage the transactions. In fact, if an error happens in the TRY..CATCH block in T-SQL, the transaction is not automatically rolled back, but it is uncommittable. You could explicitly make it committable through the use of savepoints, and then explicitly rollback to the last successful savepoint. However, to successfully use this T-SQL feature, C/AL would have to add a savepoint after every single line, and then keep track of which line executed successfully to know to which savepoint it should roll back.

Obviously, the only possible way to enable TRY..CATCH in C/AL would be to introduce explicit transactions to it. And doing that would break the whole foundation of C/AL and the fact that it is so simple, precisely because – among other things – it handles transactions completely transparently. Explicitly handling transactions would open a huge door for classes of errors unimaginable today, and it would turn programming in C/AL into a real nightmare.

And that’s why, ladies and gentlemen, there won’t be such thing as a TRY..CATCH block in C/AL. Ever. And it’s a good thing.

Vjeko

Vjeko has been writing code for living since 1995, and he has shared his knowledge and experience in presentations, articles, blogs, and elsewhere since 2002. Hopelessly curious, passionate about technology, avid language learner no matter human or computer.

This Post Has 23 Comments

  1. Gert Lynge

    Thanks for your great blog!

    As a Danish programmer used to program Classic Dynamics C5 I don’t agree that explicit transaction handling is a nightmare…
    This is what Classic Dynamics C5 requires you to do! So I know first hand :-)…
    Off cause it could introduce errors, but the benefits of far better concurrency/performance is there too…
    Most ERP code actually don’t need to run in a transaction – it you don’t belive me – learn the XAL language and look at Classic Dynamics C5 yourself :-).
    Transactions are not always used, transaction roll backs are rarely used, and XAL even has support for declaring a restart_point to handle transaction deadlocs (although it is almost never used in real life in the application).
    Don’t get me wrong – I’m learning C/AL and has been for a year – and I love it (and the great platform posibilities of webservice etc.).
    But C/AL is missing some real nice programming elements compared to XAL… Compile directives (conditions evaluated at compile time) is another one…
    Regards
    Gert Lynge

    1. Vjeko

      Thanks Gert, and welcome to my blog! Don’t get me wrong either: I am not saying the explicit transactions are a nightmare per se – they most certainly are not. In T-SQL they are definitely not a nightmare, but it heavily depends on the platform. In C/AL, introducing explicit transactions could easily break the consistency of the application, introduce tons of regression issues. Once you introduce explicit transactions, you need to have a concept of nested transactions (something that I am 100% sure 95% of people don’t fully understand), and concept of transaction counting – or you have to have your environment take care for it and make implicit decisions – which I believe is bad, because either you have it implicit, or you have it explicit – there should be no middle way. Also, in NAV there are built-in models for transaction handling with objects, such as pages, or codeunits, and introducing explicit transactions at this stage would just mean C/AL (including all of NAV application code) has to be reachitected, redesigned, and reprogrammed. I would expect to see full-fledged C# in NAV before I see C/AL TRY..CATCH.

    2. Dave Roys

      One thing that I found when switching from XAL to C/AL was that I never had to fix data from partially written transactions. I think this is something that NAV handles much better than XAL ever did (no experience of C5). However, I do wish that for those occasions where I want to trap an error, I could simply do it in code without needing to run a Codeunit.

      1. Vjeko

        I fully agree it would be very useful to have some error catching syntactical constructs available, other than codeunits. My point only was: it would come at a price.

  2. Lukas

    It would even be a handy feature though if it was used like IF Codeunit.RUN and required you to commit before using TRY. That way it wouldn’t break anything and it would save me the hassle of creating an entire Codeunit just for catching one single error. Now catching one single error uses up a licensed object, is annoying and tedious (parameter passing -.- ), confusing and an overkill.

    1. Vjeko

      That’s why I’ve given an example of five database writes in the TRY block – what if four of them succeed, and the fifth fails? It’s easy committing before the try block, but the problem is, what happens if it fails within the try block? You could argue either way, and in any case transaction should be at least uncommittable. Maybe, just maybe, if the whole thing within the TRY block would be implicitly rolled back if the CATCH block is entered – but then you have the same conceptual problem that you currently have with IF CODEUNIT.RUN within a write transaction: which part should be rolled back? Demanding COMMITs in the code requires great care while programming, because you can easily leave database inconsistent. A lot of developers would be abusing the TRY..CATCH blok at the price of a COMMIT before the block, which would in turn risk inconsistent data. I think C/AL is much safer the way it is. It’s not to say that I believe it’s the best possible thing – it definitely is not, and I am definitely not in love with it, but given circumstances and the overall architecture, it’s a good design decision that it is the way it is.

  3. TharangaC

    Thank you for your very fruitful article about error handling. It was a very interesting one and based on your blog post i have written a summarized one.
    I also agreed with you. Introducing savepoints and handling transactions manually will destroy the simplicity of C/AL programming. Plus with the current transactions, what I really love is that we do not have to worry about half cooked database writes.
    I do prefer this way rather than having savepoints and making it complex.

  4. strangenikolai

    There is a way to use a sort of TRY/CATCH block within an object as long as you don’t want to commit anything to the database (it will keep changes you make to the Global variables)

    OnRun()
    //testing a try catch scenario
    Value := ‘Is Odd?’;
    FOR i := 1 TO 5 DO BEGIN

    ASSERTERROR BEGIN //Try start
    Value := STRSUBSTNO(‘%1\ %2:’,Value,i);
    IsOdd(i);
    Value := Value + ‘+’; //will only do this code if no error
    ERROR(NOERROR); //Text Const with something unique
    END; //Try End
    IF GETLASTERRORTEXT <> NOERROR THEN BEGIN //Catch Start
    Value := Value + GETLASTERRORTEXT;
    END ELSE //Catch End/Else
    Value := Value + ‘Yes’;
    CLEARLASTERROR;
    END;

    MESSAGE(Value);

    IsOdd(value : Integer)
    IF value MOD 2 = 0 THEN
    ERROR(‘No’);

    To be fair it’s a weird little hack and I haven’t found much use for it but it does save you from creating another Codeunit for something small where you don’t know what the error will be.

    1. Vjeko

      That’s a neat trick, Nikolai! However, as you say, it will only work if no database changes are required. If you want to trap any database errors, you have to use codeunits. However, nice trick you have in your sleeve here 🙂 Kudos!

    2. strangenikolai

      Hmmm… reading that back it “cleaned up” the code when I submitted so it is missing the “not equals” operator between GETLASTERRORTEXT and NOERROR…

      1. Vjeko

        I’ve just fixed it for you – it’s that if you put < or > they are interpreted as HTML tags. If you put &lt;&gt; then it works correctly.

    3. Vjeko

      And then again, you could really put the COMMIT just before your ERROR(NOERROR) thing, couldn’t you?

      1. strangenikolai

        Oh yes – I forgot about that. It works but you need to be careful about transactions and Global variables again.

        Look at this code. If I don’t include the FIND, the message is 5 – because even though all the even numbered loops errored, the Global variable still had it’s indentation increased. If I do include the FIND it’s 3.

        OnRun(VAR Rec : Record “G/L Account”)
        //testing a try catch scenario
        Indentation := 0;
        MODIFY;
        COMMIT;

        FOR i := 1 TO 5 DO BEGIN
        ASSERTERROR BEGIN //Try start
        //FIND;
        Indentation += 1;
        MODIFY;
        IsOdd(i);

        COMMIT;
        ERROR(NOERROR);
        END; //Try End
        CLEARLASTERROR;
        END;

        MESSAGE(‘%1’,Indentation);

        LOCAL IsOdd(value : Integer)
        IF value MOD 2 = 0 THEN
        ERROR(‘No’);

        I also did a COMMIT before my block as otherwise you just don’t know what will be rolled back or not (sometimes that might be what you want – other times not so much).

        1. Vjeko

          Yes, it’s tricky, and it kind of just proves my point: TRY..CATCH has serious transaction management implications, and that’s why it is not there, and why it probably won’t ever be there. And I still say this with tongue in cheek, as I would really like Microsoft to solve all this and just prove me wrong 🙂

        2. strangenikolai

          The reason I didn’t do that (sorry to turn your comments into a big conversation) is I didn’t want to COMMIT inside the Try Catch block – I still wanted the opportunity to rollback afterwards. And with all the “gotchas” it turned out easier to restructure my code in a more “NAV way” so that other people would understand it.

          This was back in Feb 2011 I was playing with this stuff, so my memory is not great. I should probably blog these things…

          1. Vjeko

            Don’t apologize for turning this into a conversation. That’s what comments are for 🙂 Thanks for sharing this, really. It’s a neat trick, but should be used with caution. Please, do blog about it!

Leave a Reply