If this was a joke, then it would be one of those good-news-bad-news jokes. So which one do you want first? To stay true to all jokes of this kind, I’ll start with good news first.
Good news is, you now have TryFunctions, that return true if no error happens, and false if an error happens inside them.
And the bad news? You’ll never want to use them.
Last year about this time, I posted the Try..Catch in C/AL post here on my blog. In it, I made a brave claim – that we’ll never have try..catch in C/AL, and I explained why not. I also said I’d like to be wrong with that one, and on the face value of it, as José shows in his How To: Try-Catch in C/AL for NAV2016 blog post, it seems I was proven wrong, after all.
But I wasn’t wrong. And I said I was sure about not being wrong.
And now my brain hurts. Not because I was wrong and I was now proven wrong. No, I don’t mind being proven wrong. My brain hurts because I still can’t wrap it around the way how TryFunctions are implemented.
So, let’s first take a look at a simple example that showcases the feature. I’ll use the same example José did on his blog, only I’ll alter the name of the villain:
When you run this, you see this:
And you may go “wow, a cool feature!”
And it would be “wow” if the code we write didn’t have something to do with the database and transactions.
So, let’s complicate things a tiny bit. Imagine you have a simple table:
And then imagine you have this code:
While all try functions duly report failure, when you run the Try Table, you end up with this:
Now imagine it wasn’t a demo table and a demo codeunit. Imagine it was table 17 and codeunit 12. Imagine… (you won’t have to imagine for too long, I promise…)
If you don’t see a problem, then spend a minute contemplating the code example above. If you still don’t see the problem, then let me help you.
There are a lot of database operations in this example. Each TryFunction has at least three database operations, and while in the laboratory conditions of this simple example it may be obvious which ones will fail, in real life you can’t really tell.
Make a ridiculously simple change in this example, and you can’t tell what’s gonna happen anymore:
Just comment this line, and you may have total mess.
So, what you end up here is unpredictable code. It may fail at every single last one of database operations in the example, and you have no way to predict either where it will fail, and what data will result from the whole circus. And – what’s the worst – if you don’t do anything, data is happily committed regardless of data errors. And to be even worse than the worst – if you want to properly clean up, without cleaning up everything (which is the only possible explanation I can find for the current behavior) then I have nothing more to say but – good luck!
At this point, I’ll allow you to quote the documentation into my face:
Changes to the database that are made with a try function are not rolled back.
Say what? Tell me it’s not April 1st or something…
With that statement, one important concept goes right out of the window: transactions.
If you know the first thing about transactions, then at this point you are scared shitless, as am I. If you don’t know the first thing about transactions, then please read the very first thing Wikipedia has to say about transactions:
A database transaction, by definition, must be atomic.
If you are unsure what atomic means, click the link above.
If you still don’t know the first thing about transactions, then please read the very first thing SQL Server documentation has to say about transactions:
A transaction is a single unit of work. If a transaction is successful, all of the data modifications made during the transaction are committed and become a permanent part of the database. If a transaction encounters errors and must be canceled or rolled back, then all of the data modifications are erased.
Now please read once again the statement from NAV documentation:
Changes to the database that are made with a try function are not rolled back.
If there was one thing in C/AL that you could trust your life with, that was transactions. They worked like a charm, and there was no way to violate the transactional principle of atomicity, and C/AL as a language made it impossible to mess things up. It kept transactions as sacrosanct as not allowing you to possibly enter into a situation where you would end up with confusion as to which data should be retained, and which should be deleted (remember having to COMMIT before calling IF CODEUNIT.RUN).
You can’t depend on it anymore. With TryFunctions, your transactions aren’t atomic. While Ernest Walton and John Cockcroft got Nobel prize in physics for atom splitting, I don’t think anybody will be getting any prizes for allowing us to split, or better yet, shatter atomic database transactions into tiny shards that we can’t keep proper track of anymore. Depending on the level of our inspiration at the time of writing our code, we may end up with a mess that no customer in their right mind would want to watch about in “Science of Stupid”, let alone see alive in their database.
Well, to be completely honest – SQL Server is the guy that allows this behavior. There is one little nasty guy called XACT_ABORT that has been around in SQL Server since version 2005, and it controls the transaction behavior. If it is set to OFF, then SQL Server will only rollback that statement which caused error, but will commit any data successfully written before or after the statement that caused the error, unless transaction is explicitly rolled back. Now, I can’t make categorical claims here, but my guess is that in earlier versions of NAV, XACT ABORT was ON, and now XACT_ABORT is OFF.
At the very minimum I can claim this: XACT_ABORT in NAV 2016 certainly is OFF, and NST does not explicitly roll back if error is encountered inside TryFunction. While it might have still been set to OFF in earlier versions of NAV, NST (and earlier Classic client) were for sure explicitly rolling back on any error. My hunch tells me that XACT_ABORT was ON, but I can’t now test to verify this assumption (simply because I don’t have time).
But regardless of how NST handles transactions, implicitly or explicitly, with or without XACT_ABORT – my opinion is that it’s a fundamental mistake to allow C/AL to behave the way it behaves in build 42815.
The reason why I believe so is transactional integrity (I know, I am boring already with this demand for transactional integrity, while SQL Server obviously allows us to set this behavior on or off). However, there is a major difference between SQL Server and C/AL: SQL Server allows us complete freedom in explicitly handling transactions and achieve behavior that we want; C/AL doesn’t. There are no explicit transactions in C/AL.
While it may seem that transactions aren’t that atomic, since SQL allows individual operation failure to not affect the outcome of the whole transaction, XACT_ABORT in fact does not violate anything – it merely allows more syntactical freedom in T-SQL. What it does is, it saves us from having to use TRY..CATCH and beginning/committing/rolling back individual nested transactions, and that’s all. As a T-SQL developer, you have a choice between the two models, and an arsenal of features to control the transaction flow to the tiniest detail.
In C/AL, the only thing we can explicitly do to a transaction is to commit it. With ASSERTERROR ERROR(‘’) trick we can also explicitly roll it back, but this is a hack, rather than a designed feature. (Yes, we can explicitly roll it back with any error, but it also stops the transaction and as such does not really work as a transaction control mechanism.)
C/AL as a whole is designed to maintain and protect transaction integrity, so that as developers we don’t have to keep track of whether something, somewhere, went wrong and then depending on that having to decide which database write operations to retain (commit) and which to reject (roll back).
TryFunctions change it all.
And I am not arguing here that we should get more freedom with transaction control in C/AL, and that this would make TryFunctions good. No – I am arguing that this kind of feature in C/AL is inherently dangerous.
My strong position is that the only correct way to use TryFunction is in the following pattern:
If you don’t always do this, then you risk errors the kind of which you cannot begin to predict.
To show why, let me take the very example from the MSDN documentation for the TryFunction feature:
Yes, I slightly modified it (if you take the original, you’ll immediately know why) – but I didn’t change its core mechanics – it’s still exactly the same example.
And now run that code you just wrote above.
The next thing that happens is this:
If you try to run it again, you get this:
And then immediately this:
If you try to post this order manually from the order list, then you don’t get the CONSISTENT error, but you do get the error with the same message as above – which is correct behavior.
Then, if you try to delete this order to conceal the mess, you get this:
Then the same about the invoice.
And then the order remains in the database.
Whatever ensues, and if this was production, I don’t want to be you. I actually want to grab some popcorn, sit and watch the show. Your best bet is stopping the NST (or all of them), restoring your last database backup, and then rolling forward the transaction log to the second before your unlucky little TryFunction experiment.
And if all this happens with a very simple, textbook example, just hope it won’t ever, ever, ever happen to you in your production database.
Okay, yes, you could fix the problem easily if you do this:
But can you know, for a fact, that everybody who calls your TryPosting function will do that? And everyone who calls the function that calls their function that calls TryPosting? And so on, ad as often as unnecessary.
My point here is – TryFunction is a huge backdoor for transactional inconsistencies and bugs and if you are using it carelessly, you will be sorry. Very, very sorry.
Now that you know what kind of Trojan horse this TryFunction fellow is, you can still take advantage of it, in the following situations:
- Your TryFunction code has absolutely nothing to do with the database. As long as your errors do not result from database operations, TryFunction is safe.
- You always, absolutely, and without exception make all your TryFunctions local, and always, absolutely, and without exception roll back the transaction after receiving FALSE from it. Still, this is unsafe, because you can’t be 100% sure somebody else will not simply call your function and not roll back the transaction. But this is at least somewhat safe.
All other situations should raise a big fat red alert, and please don’t say you haven’t been warned.
Microsoft could fix this easily: when an error happens inside a TryFunction, and the error was caused by a database operation, transaction should be marked as uncommittable (just as SQL Server does with try..catch). If an error was caused by a non-database operation, then we should still have control (through a property?) over whether we want the transaction to be uncommittable or not. While this would certainly be a big paradigm change (having uncommittable transactions) in C/AL, it’s certainly nothing compared to the paradigm change of non-atomic transactions.
What do you think about all this? Is this much ado about nothing and you’ll still happily TryFunction around your code, or you will avoid using it if at all possible? Please – share your thoughts.