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:
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:
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:
Exactly. The mother of all ugly errors will happen:
“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.
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
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.
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.
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.
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.
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.
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.
Pingback: Why no Try and Catch? - Microsoft Dynamics NAV Community
Pingback: TRY.. CATCH in C/AL – 10/17, Navigate Into Success |
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.
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!
Hmmm… reading that back it “cleaned up” the code when I submitted so it is missing the “not equals” operator between GETLASTERRORTEXT and NOERROR…
I’ve just fixed it for you – it’s that if you put < or > they are interpreted as HTML tags. If you put <> then it works correctly.
And then again, you could really put the COMMIT just before your ERROR(NOERROR) thing, couldn’t you?
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).
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 🙂
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…
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!
Pingback: How To: Try-Catch in C/AL for NAV2016 - Microsoft Dynamics NAV Community
Pingback: What’s New in NAV 2016: Splitting Atoms with TryFunction - Vjeko.com (a Microsoft Dynamics NAV blog)
Pingback: My two cents about Try/Catch in C/AL | NAV NAB BLOG
Pingback: The type or method ‘TryFunction” cannot be used for ‘Extension’ development - Microsoft Dynamics 365, Business edition Community
Pingback: Dreaming about C/AL | The Dynamics Tailor