Introduction
Winter 22 introduces the capability to
roll back pending record changes when a flow element fails at run time. I had two reactions to this - first, that's good; second, how come it
didn't always do that? It took me back to the (very few) days when I did
Visual Basic work and found out that
boolean expressions didn't short circuit
- it's something that has been such an integral part of other technologies
that I've worked with, it never occurred to me that this would be different.
The original idea for this post was to call out the roll back only applies to
the current transaction, and tie in how Apex works as a comparison. But then it occurred to me that in the
Salesforce ecosystem there are many people teaching themselves Apex who could
probably use a bit more information about how transactions work, So, in the words of Henry James wrote in his obituary for George du Maurier
(referring to the novel
Trilby):
"the whole phenomenon grew and grew till it became, at any rate for this
particular victim, a fountain of gloom and a portent of woe"
Transactions
A transaction is a collection of actions that are applied as a single unit.
Transactions ensure the integrity of the database in the face of exceptions,
errors, crashes and power failures. The characteristics of a transaction are known by the acronym ACID:
-
Atomic - the work succeeds entirely or fails entirely. Regardless of what
happens in terms of failure, the database cannot be partially updated.
-
Consistent - when a transaction has completed, the database is in a valid
state in terms of all rules and constraints. This doesn't guarantee the data
is correct, just that it is legal from the database perspective.
-
Isolated - the changes made during a transaction are not visible to other
users/requests until the transaction completes.
-
Durable - once a transaction completes, the changes made are
permanent.
Transactions in Apex
Apex is different to a number of languages that I've used in the past, as it
is tightly coupled with the database. For this reason you don't have to worry
about transaction management unless you want to take control. When a user
makes a request that invokes your code, for example a Lightning Web Component
calling an @AuraEnabled method, your code is already in a transaction
context.
When a request completes without error, the transaction is automatically
committed and all changes made during the request are applied permanently to
the database. This also causes work such as sending emails and
publishing certain types of platform events to take place. Sending an email
about work that didn't actually happen doesn't make a lot of sense, although
this caused us plenty of angst in the past as we tried to find ways to log to
an external system that a transaction had failed (Publish Immediately platform
events finally gave us a mechanism to achieve this).
When a request encounters an error, the transaction is automatically rolled
back and all changes made during the request are discarded. Often the user
receives an ugly stack trace, which is where you might decide that you need a
bit more control over the transaction.
Catching Exceptions
By catching an exception, you can interrogate the exception object to find out
what actually happened and formulate a nice error message to send to the user.
However, unless you surface this message to the user, you have changed the
behaviour of the transaction, maybe without realising it. For example, if you
catch an exception and return a message encapsulating what happened:
Account acc=new Account(Name='TX Test');
Contact cont=new Contact(FirstName='Keir', LastName='Bowden');
String result='SUCCESS';
try {
insert acc;
cont.AccountId=acc.Id;
insert cont;
}
catch (Exception e) {
result='Error ' + e.getMessage();
}
return result;
In my dev org, I have a validation rule set up that one of email or phone must
be defined, so the insert of the contact fails and I see a fairly nice error
message:
Unfortunately, this isn't the only issue with my code - as I caught the
exception, the request didn't fail so the transaction wasn't automatically
rolled back. While from the user's perspective the request failed, the first
part of it succeeded and an account named TX Test was created:
and every time the user retries the request, I'll get another TX Test account
and they will get another error message.
Taking Back Control
Luckily Apex has my back on this, and if I want to (and I know what I'm doing)
I can take control of the transaction and apply some nuance to it, rather than
everything succeeding or failing.
Savepoints allow you to create what can be thought of as sub or nested
transactions. A savepoint identifies a point within a transaction that can be
rolled back to, undoing all work after that point but retaining all work prior
to that point. Changing my snippet from above to use a savepoint and
rollback, I can ensure that all of my changes succeed or fail :
SavePoint preAccountSavePoint=Database.setSavePoint();
Account acc=new Account(Name='TX Test');
Contact cont=new Contact(FirstName='Keir', LastName='Bowden');
String result='SUCCESS';
try {
insert acc;
cont.AccountId=acc.Id;
insert cont;
}
catch (Exception e) {
result='Error ' + e.getMessage();
Database.rollback(preAccountSavePoint);
}
return result;
The user's experience remains the same - they receive a friendly error message that the insert failed. This time, though, there is no need to rid my database of the troublesome account. Prior to carrying out any DML I have created a Savepoint, and in my exception handler I rollback to that Savepoint, undoing all of the work in between - the insert of the account that was successful. Note that rolling back the transaction has no effect on my local variables - prior to the return statement I have an account in memory that has an Id assigned from the database, but that doesn't exist any more. Of course this also leaves any other work that happened in the transaction outside of my code in place, which may or may not be what I want, so taking control of transactions shouldn't be done lightly.
Savepoints are also useful if there are a number of courses of action that are equally valid for your code to take. You set a Savepoint and try option 1, if that doesn't work you rollback and try option 2 and so on. This is pretty rare in my experience, as usually there is a specific requirement around a user action, but it does happen, like when option 2 is writing the details of how and why option 1 failed.
You can also set multiple Savepoints, each rolling back less and less work, and choose how far rollback to in the event of an error. Your co-workers probably won't thank you for this, and are more likely to see it as combining Circles of Hell into your very own
Inferno. If you do choose to go this route, note that when you rollback to a savepoint, any that you created since then are no longer valid, so you can't switch between different versions of the database at will.
Savepoints only work in the current transaction, so if you execute a batch job that requires 10 batches to process all of the records, rolling back batch 10 has no effect on batches 1-9, as those took place in their own transactions which completed successfully.
Rolling Back Flow Transactions
After that lengthy diversion into Apex, hopefully you'll understand why I expect everything to rollback the transaction automatically when there is an error - it's been a long time since it has been my problem. Per the Winter 22 release notes, flow doesn't do this :
Previously, when a transaction ended, its pending record changes were saved to the database even if a flow element failed in the transaction
and bear in mind that is still the case - what has been added is a new Roll Back Records element you can use in a fault path. Why wasn't this changed to automatically roll back on error? For the same reason that Visual Basic didn't start short circuiting boolean expressions - there's a ton of existing solutions out there and some of them will rely on flow working this way. While it's not ideal, nor is introducing a breaking change to existing customer automation.
Something else to bear in mind is that this rolls back the current transaction, not necessarily all of the work that has taken place in the flow. Per the Flows in Transactions Salesforce Help article, a flow transaction ends when a Screen, Local Action or Pause element is executed. Continuing with my Account and Contact approach, if you've inserted the Account and then use a Screen element to ask the user for the Contact name, the Account is committed to the database and will persist regardless of what happens to your attempt to create a Contact. Much like the batch Apex note above, rolling back the second (Contact) transaction has no effect on the first (Account) transaction as that has already completed successfully.
Enjoying Transactions?
Try distributed transactions:
And then scale them up across many microservices.
Many years ago I used to have to manage transactions myself, and in one particular case I had to work on distributing transactions across a number of disparate systems. It was interesting and challenging work, but I don't miss it!
Related Posts