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.
Anyway – don’t run it yet. If you do, it’ll go and successfully post this document. Instead, modify codeunit 90, by appending this:
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.
This Post Has 49 Comments
First of all: Again an excellent post ????
I’ve also discovered this with the TryFunctions, but as you said, when using it correctly it does have the benefit to catch some simple .net errors like doing some file operations using System.IO.File
But again, you have to be damn sure you’re not inside a database transaction!
I’m sure that, since Microsoft has now created this security hole, developers who don’t care about reading any blogs or help will at some point definitely fall into this hole and cause serious problems ????
Peter – that’s the problem. The more I think of it, the more I see this should not have happened – period. I am also totally in for catching .NET errors (why would some innocent call to a .NET object cause the transaction to fail?) but not at the cost of transaction consistency…
Yeah, its halfbaked – Just written to catch some nasty exceptions from .NET…
Yes, I instantly thought the same thing- I will use it only in purpose to catch .NET exceptions, which now I catch with the help of: IF NOT Codeunit.RUN…So the advantage is one saved Codeunit
Great post, well explained and couldn’t agree more.
Note taken, Vjeko. Thanks for this loud and clear advice 🙂
Thanks Vjeko! Excellent blog.
I could think of a test framework were you would actually want to keep the erroneous transaction as proof to why the code failed, and also for later scrutiny. In a production environment however, it is a completely different story.
Thanks, Tim! 🙂
Excellent writeup, Vjeko.
I haven’t yet had a chance to deep dive with this particular feature, but if what you are describing here is correct, this is only useful for non-database operations (or the ones that do not write any data).
So, to recap:
1) as long as the database write operations are concerned, the TryFunction seems to be just a glorified version of “IF INSERT/MODIFY/DELETE THEN” statement :).
2) in all other cases, it’s a slight improvement over IF CODEUNIT.RUN() expression
Thanks, Vytenis. About your recap, I don’t quite agree that IF TryFunction() THEN is just a glorified version of INSERT/MODIFY/DELETE THEN. It would have been exactly that, if a failed TryFunction would rollback everything that was written inside of it. It doesn’t. When IF INSERT/MODIFY/DELETE fails, you don’t have to clean up anything. And if you have a series of consecutive IF INSERT/MODIFY/DELETE calls and only some fail, it’s fairly easy to keep track of those that succeeded, so you can clean them up later if necessary. With IF TryFunction() you have to put in much more effort to keep track and clean up. Proper comparison between IF TryFunction and others would be as if DELETEALL and MODIFYALL supported IF … construct (which they don’t) and they didn’t cause a failure in case one or more individual modifications/deletions failed. That would be a disaster, wouldn’t it? So – no, it’s not just a glorified version of IF INSERT/MODIFY/DELETE…
I totally agree with the clear and loud message.
If the platform is not preventing you from splitting atomic transactions or giving you control over it, then this all depends on mutual agreement between developers. That will soon or later fail.
And I definitely don’t want to be in the position to explain this to customers or to clean up the mess.
So for our development and code reviews, we will choose to not allow any database transactons in a TryFunction.
Thanks Vjeko! I fully agree on your warnings. My first Thought when I saw the documentation to TryFunctions was “Just a 1.April Joke for Betatesters”…
But did you know, that this isn’t the first time C/AL let you split a transaction? You could easily write a codeunit with just a Rec as parameter and an INSERT as the only line of code. Then call this via STARTSESSION on a fresh populated record and the record will always be inserted and never rolled back, regardless of how many errors you raise after that startsession… On the other hand, an error in the started session while inserting that record won’t raise an error and won’t cause a rollback. Also very dangerous if not used wisely (I used it mainly for writing individual process log entries).
Markus – that’s a bit different. STARTSESSION is a new session, a new thread talking to SQL, so – separate transactions. Even though you called STARTSESSION from transaction A, whatever happens inside that session is a separate transaction, and not part of transaction A anymore, so error on transaction A won’t affect anything that happened in transaction B. That’s why it’s not really breaking transaction integrity.
First things first, thanks for mentioning me.
After carefully going through your post I have to agree with you.
I still do not feel that TryFunctions are a bad idea though. They are incomplete (to me).
From now on I will use those type of functions to do testing that does not require actual changes to the database and ensure that the data that will be posted for instance, is correct.
I do not know if it might be taking it a little too far, but it makes sense to me to think that an improvement that could be made would be not allowing any kind of INSERT/MODIFY/DELETE/RENAME/… unless it’s on a temporary table for instance. Raising a compilation error.
In any case, I leave this post knowing more than I did when I came to it. Good stuff!
Jose – any error given due to database opereations inside TryFunction should not be compilation, but runtime. A TryFunction could call other function that calls other function, and you could have have a database write operation deep in the call hierarchy, still under a TryFunction that initiated the call. Compiler should not be checking for these conditions as they are extremely volatile. TryFunctions should only work for non-database operations, and if any database operation is encountered, they should unconditionally rollback everything and throw a fat ugly error to indicate that code refactoring must follow.
I think there’s something similar (but don’t know exactly how it looked like since I don’t encounter it anymore 😛 ):
“This message is for C/AL programmers……
This is a programming error…”
It’s easy to reproduce this one. Just start a database write transaction and then do IF CODEUNIT.RUN(…)
By the way – two of your comments ended in spam, and I almost deleted them. I don’t know what’s wrong with WordPress these days…
That’s easy Vjeko, I’m simply blacklisted ???? We were also already blacklisted within the NAV TAP program by Microsoft with 2015 ????
Whoever blacklisted you for TAP has no say here on my blog. You must have attempted to sell some suspcious pharmaceuticals to maintain integrity of certain non-transactional states, and got caught by the spam algorithm ;-P
I like that a … a fat ugly error… must say.. I was quite happy to see the tryfunction concept – didn’t think that it would be implemented as it is though. I would expect full rollback – and I would commit prior to call it if I wanted committed data. Leaves a massive possibility of a mess – you are 100% right.
Hi Vieko,great article as usual. It seems that Microsoft is recently trying to brake the basic Nav behaviour…
Another thing I don’t like is that records for uncommitted transactions (in progress) are “visible” to other sessions.
With old native this was not possible. The basic idea that if you have a balance for a g/l account and at later time this balance is changed you should see an additional line which caused the balance to be changed.
If you look at gl entries you might see records which will no more be there after the uncommitted transaction is rolled back.
Other weird behaviour is the execution of code in table triggers in the context of current Company when you use changecompany on a recordref.
Gianluca, I agree that phantom records may be confusing, but it’s about transaction isolation, and depending on how far you want to go, you can control it pretty well. You should always be aware that transaction isolation always comes at cost of performance and concurrence, so you should weigh your choices carefully. With native this was not possible because it used it’s “versioning” isolation that prevented phantoms, but also couldn’t scale as well as SQL can.
Yes, now Nav can scale up much more. Very great features also in 2016, especially extensions and events. Still investigating them but it seems a big step in the right direction. Not sure if you can use them to modify some standard behaviour
Events are great – they don’t modify the standard behavior, they extend it. You can’t use events to change standard behavior unless you change something in the standard behavior (or if standard behavior is architected in a way that a change in context passed on from the event will influence code branching made by the standard feature).
Pingback: NAV 2016 on MVP’s blogs | Totovic NAV Blog
Great post, Vjeko!
I agree, as currently implemented, TryFunctions should definitely not manipulate the database for reasons well explained above. (For the same reasons, TryFunctions should not publish any events, as you have no control over what subscribers will do.)
Maybe Microsoft could consider to improve the TryFunction-implementation with SQL Server savepoints. This will ensure that transactions are atomic and is still implicit from a developer perspective, i.e. no need for explicit commands for beginning/ending/rolling back transactions.
Here is how it could work:
* When a TryFunction begins, the C/AL runtime will use SQL Server’s SAVE TRANSACTION
* If the TryFunction fails, the C/AL runtime will use SQL Server’s ROLLBACK TRANSACTION
* If the TryFunction succeeds, the C/AL runtime will continue and never use the savepoint for anything
The above implementation can be optimized, so the actual SAVE TRANSACTION is postponed until the first DML statement sent to the database.
An implementation like this will ensure that the database is always left in a consistent state: When the TryFunction is left, the database will either have been “restored” the state when to the point at which the TryFunction began, or all the database operations have been done and will all be committed or rolled back depending on what happens afterwards.
With such implementation, the ‘Try Table’ example will end up with records 1 and 2 (inserted by PrepareData) as all TryFunctions fail and therefore roll back to their initial state.
Thanks, Christian! Yes, your suggestion would work, that would make transactions consistent with the code flow and what most developers would expect. But still, it would be somewhat dangerous, depending on circumstances. In my Try Table example – it may be okay that records 1 and 2 stay in there in case try functions fail. But what if it is incorrect? A real world example perhaps: imagine that codeunit 80 is a smorgasbord of try functions – some succeed, some fail – whatever you get as an end result can easily be the same kind of mess we should avoid. In my firm opinion, nothing should be implicit, and if anything is, then it has to be that transactions become uncommittable (as is the case with SQL). The next best solution is to give us explicit control of the transaction flow, but I don’t think for C/AL it would be a good idea.
Excellent post, and should be made mandatory reading for all NAV devs, and not just us Grumpy ones 🙂
I agree Peter 🙂
Thanks, Peter! But you are not nearly as grumpy as your nickname suggests 😉
As above excellent post.. This should only be used in non-transactional situations (does this exist? :))..
It does, believe it or not 🙂
I guess the big question is: is MS already using this in NAV standard code? Whatever we discuss here is moot if they already put that stuff in the standard application (with or without database transactions)
They are, of course. They never introduce a new C/AL feature just because. When they do it, they always use it. And this particular one, they use it both with and without transactions.
And they use it quite a lot 😐
As a matter of fact, they use it in 84 objects(!)
I especially ‘like’ this one in Codeunit 81:
OnRun(VAR Rec : Record “Sales Header”)
Rec := SalesHeader;
LOCAL [TryFunction] Code()
WITH SalesHeader DO…
Now, MS has gone through quite some length to make sure this works without issues (because they use it for the new functionality of Preview Posting), but think about when ‘some’ developer says: “Hey, this is cool, let’s make a new global function in this codeunit that ‘tries’ posting a Sales Document using the IF Code THEN..” 🙁
I would say: “Run! Forrest, Run!…”
Yes, run, Forrest, run… This calls for another blog post.
Really strange. Would be interesting to know Microsofts real reasons for this decision. Anyway, developers should be very careful to use it.
Thanks! I’ll keep my fingers crossed that this was merely a mistake, not an intentional design decision.
Pingback: TryFunction Lessons Learned from Preview Posting - Vjeko.com (a Microsoft Dynamics NAV blog)
the picture after “Okay, yes, you could fix the problem easily if you do this:” looks exactly the same as the original code?
Otherwise, great post.
No, it’s not the same. The new editor even shows you exactly what’s different. Give it another try 😉 The difference is major.
Pingback: Nav Square Microsoft Dynamics NAV 2016 : What’s new for developers? - Nav Square
Pingback: NAV 2016 and TryFunctions: epic fail? | Stefano Demiliani
Excellent post Vjeko, really informative. At first I thought that I would have wanted to using the try-catch functionality on non-transaction functions only, but after reconsidering I don’t think the risk/reward pays off. Besides, this functionality has not really existed before and complex solutions have been created without it.
Excellent post! For ALL NAV developers.
Thanks… Perfect explanation with so many valuable details. No doubts!
I can agree this functionality generally could be very dangerous. But I can imagine some very specific scenarios where using it could simplify the solution. One (and for me right now this the only one solution I would use it) case could be some king of integration between two transaction independent platforms (NAV + other platform/solution). Let`s imagine you need to call some external function that will cause return some bunch of data. One part of the data could be useful to store and update so an existing entries in NAV but another one should raise an NAV error. Try functions give you possibility to do it in a very elegant way. Of course, you should encapsulated NAV function as a private function to be accessible from the specific object only and use it in this way just in the specific situation.
But yes, generally I will avoid using this functionality.
It’s true, anyone can find some very specific scenarios where using it could simplify things. However, for every legitimate specific scenario where it helps, there are thousands where this functionality is outright dangerous and shouldn’t be used. Therefore – it should be changed. Or we should be very much aware of what it is and does before doing anything.
However, in the world full of events and extensions, it’s very difficult to make 100% sure that your try function which never intended to do transactions suddenly ends up doing data modification through some extended+evented layer somewhere.
Thanks Vjeko! Any update on this? Later builds? Later versions?
Yes – the news is that in 2017 and onwards the default behavior is that no database operations are allowed through the TryFunctions. There is a server setting in the server configuration xml file that can change that, but I am intentionally not writing about it, because you should not touch that setting.