Today’s topic is an Apex feature that actually came out during the Summer ‘21 release; we will be focusing on Apex Transaction Finalizers – part of the Asynchronous Apex.
In addition to this topic, there are many recent features and enhancements to Apex that I’m excited about! You can refresh your knowledge here.
Why Do You Need Finalizers?
For any developer who has worked with Async Apex before, while it is very powerful and offers different tools depending on your use case, there has been a historical problem with it: it’s difficult to implement retry logic doing callouts and logging.
Admins and developers should also know that you can monitor these async transactions via the Apex Jobs tab on your Setup page – this will help you see what’s happening in the org, but what if you need more control over these transactions?
For example, if you’re using Future methods, it doesn’t return an Apex job ID by design. While it’s great to have these tools, what if you need better logging and retry logic and want to rely less on Batch Apex? This is where Queueable Apex and Finalizers come in.
Before Transaction Finalizers, you had to query AsyncApexJob’s ID via SOQL and implement a retry logic in Queueable Apex. One can also argue since Batch Apex has its own limits and you can only queue and simultaneously run five of them in your org, having enhancements to Queueable Apex is amazing to have as an alternative.
For simple Async Apex processing and making callouts to other systems with low data volumes, Queueables are great. Before Finalizers, however, when things got too complex where you had to chain queueable jobs, if one of these failed due to limit exception (or any other reason), you couldn’t continue and had to retry the whole chain.
What Do Finalizers Do?
Transaction Finalizers allow you to attach actions to Queueable Apex jobs using the Finalizer interface. When a job that uses this interface finishes executing, whether successfully or with an error, the associated Finalizer implementation runs, enabling you to perform clean-up or recovery operations.
Finalizers allow you to accomplish various tasks such as sending email notifications to keep track of activities, maintaining comprehensive logs for debugging and monitoring, automatically retrying the Queueable in the event errors, and making external callouts to other systems even after DML operations have been executed in the Queueable context.
This is huge and it’s worth noting that performing a callout after a DML operation is not allowed in Apex. However, this feature provides a workaround for that constraint since it runs after the Queueable job is finished.
Finalizers execute not only when a job fails, but also when it successfully completes. This provides an ideal moment to carry out additional tasks after the transaction is done. For example, you could log important data, add remaining API calls, or perform any other post-transaction operations that may be necessary.
How Can I Use Finalizers?
Here’s a sample for retry logic for your Queuable Apex from the documentation:
public class RetryLimitDemo implements Finalizer, Queueable {
// Queueable implementation
public void execute(QueueableContext ctx) {
String jobId = '' + ctx.getJobId();
System.debug('Begin: executing queueable job: ' + jobId);
try {
Finalizer finalizer = new RetryLimitDemo();
System.attachFinalizer(finalizer);
System.debug('Attached finalizer');
Integer accountNumber = 1;
while (true) { // results in limit error
Account a = new Account();
a.Name = 'Account-Number-' + accountNumber;
insert a;
accountNumber++;
}
} catch (Exception e) {
System.debug('Error executing the job [' + jobId + ']: ' + e.getMessage());
} finally {
System.debug('Completed: execution of queueable job: ' + jobId);
}
}
// Finalizer implementation
public void execute(FinalizerContext ctx) {
String parentJobId = '' + ctx.getAsyncApexJobId();
System.debug('Begin: executing finalizer attached to queueable job: ' + parentJobId);
if (ctx.getResult() == ParentJobResult.SUCCESS) {
System.debug('Parent queueable job [' + parentJobId + '] completed successfully.');
} else {
System.debug('Parent queueable job [' + parentJobId + '] failed due to unhandled exception: ' + ctx.getException().getMessage());
System.debug('Enqueueing another instance of the queueable...');
String newJobId = '' + System.enqueueJob(new RetryLimitDemo()); // This call fails after 5 times when it hits the chaining limit
System.debug('Enqueued new job: ' + newJobId);
}
System.debug('Completed: execution of finalizer attached to queueable job: ' + parentJobId);
}
}
Queueable Context: This contains our usual method execute(QueueableContext ctx) required by the Queueable interface. This method is the starting point when the job is enqueued.
Finalizer Context: Code also contains a method execute(FinalizerContext ctx) required by the Finalizer interface. This method is executed when the Queueable job either succeeds or fails.
Note that the queueable class implements the Finalizer interface and System.attachFinalizer(finalizer); attaches a Finalizer to the current Queueable job. This Finalizer will be executed after the Queueable job is completed (either successfully or with a failure). We initiate a new finalizer using Finalizer finalizer = new RetryLimitDemo();
In the Queuable execute method, we’re getting a limit error and enqueuing a new Queueable by using the finalizer interface by String newJobId = ” + System.enqueueJob(new RetryLimitDemo(). Yes, it’s that easy!
Best Practices and Considerations
Only one finalizer instance can be attached to any Queueable job. When I tested it to see what happens if you attach more than one finalizer (why not?), the transaction failed as expected, but the first one still fired. Can you guess why? It’s because transaction finalizers run whether the attached apex job fails or succeeds!
You can enqueue a single asynchronous Apex job (Queueable, Future, or Batch) in the finalizer’s implementation of the execute method. This can be used for a single Async Job that checks the status of all others and Async jobs in the system – neat!
Callouts are allowed in finalizer implementations. This lets us commit to the database and still make callouts in the finalizer context. You can have DML operations within a Queueable job, whereas a Finalizer can perform REST API callouts.
Summary
In Salesforce, Queueable jobs and Finalizers operate within distinct Apex and Database contexts. This is how Finalizer can run after an uncaught error because it runs in its own execution context. This means that the original queueable class instance that instantiated the finalizer no longer exists, so you should avoid trying to call back into the queueable class from the finalizer. I’m really happy with this feature and hope that it gets expanded to other features of the platform, for example, Apex triggers.
At the moment, the ability to handle uncatchable Apex errors this way without incurring an extra 10% expense on your overall Salesforce budget for Event Monitoring is invaluable in itself. If this feature is expanded, we can have better error handling and database control. While we hope for new features, let’s thank the Apex team that has been doing great work in recent years.
If you’re looking to learn more about development or any topic about Salesforce, feel free to join me and others at SFXD, a community of Salesforce professionals. We currently have 10,000+ members, with a very active core membership connecting every day on our Discord server.
If you’re looking for an integration solution for your Salesforce org, I also recommend Declarative Webhooks which helps you easily build robust integrations between Salesforce and other platforms without code.
I hope you enjoyed this article – see you for the next one!
The Author
Atlas Can
Atlas works as a 10x certified Salesforce Developer at Omnivo Digital and active on communities including his community SFDX.