Understanding "Uncommitted Work Pending" Exception

Understanding "Uncommitted Work Pending" Exception

Introduction

In the world of Salesforce development, understanding and handling exceptions is crucial for creating robust and efficient applications. One such challenging exception that developers often encounter is the "Uncommitted Work Pending" exception. This error typically arises when developers attempt to make a callout after a Data Manipulation Language (DML) operation within the same transaction. However, the catch is that this restriction is not limited to DML operations alone.

The Core Issue

Before diving into the specifics of the "Uncommitted Work Pending" exception, it's essential to understand what it signifies. At its core, this exception is Salesforce's way of maintaining data integrity and transactional consistency. Whenever a DML operation occurs — such as insert, update, or delete — Salesforce locks in that transaction to ensure data consistency. If a callout is made after this operation within the same transaction, it poses a risk of data inconsistency, hence the exception.

However, it's crucial to recognize that this limitation extends beyond just DML operations. Other asynchronous operations, including Future Jobs, Batch Jobs, Queueable Jobs and PlatForm Events (publish after commit) can also trigger this exception. This is because whenever we fire these methods,

  • Future

  • Database.executeBatch

  • System.enqueueJob

  • EventBus.publish (only for publish after commit)

salesforce internally does a DML on asynchronous queue for these jobs to be executed after the current transaction is finished. This broader impact underscores the need for developers to understand Salesforce's order of execution and transaction control deeply.

Understanding Transaction Control

Salesforce maintains a strict order of execution for operations to ensure data integrity. When a transaction begins, and a DML operation is executed, Salesforce marks the transaction as having pending work that must be committed or rolled back entirely to maintain data consistency. Any attempt to initiate an external callout after this point is flagged as risky and results in the "Uncommitted Work Pending" exception.

The Solution: Platform Events (Publish Immediately)

Now, if you really need to carry out any DML operation such as logging etc, One effective workaround to this exception is the use of Platform Events with the "publish immediately" option. Platform events are events that communicate changes or actions within Salesforce. While the default behavior is "publish after commit," which would fall into the same restrictions as DML operations, the "publish immediately" option allows the event to be sent out immediately without waiting for the transaction to complete.

Demo

I created a LWC Component to demonstrate this.

Each button invokes a different transaction on server size.

Here is the code snippet. Here I am trying to do a showcase by initiating different transactions and then doing a simple rest api call.

/**
 * Created by Nagendra on 05-01-2024.
 */

public with sharing class CheckUncommittedWorkPendingError {

    @AuraEnabled
    public static String initiateJobAndDML(String jobName){
        String strMessage = 'Success';
        try {
            switch on jobName {
                when 'future' {
                    initiateFutureJob();
                    initiateHTTPRequest();
                }

                when 'batch' {
                    initiateBatchJob();
                    initiateHTTPRequest();
                }

                when 'queueable' {
                    initiateQueueableJob();
                    initiateHTTPRequest();
                }

                when 'peAfterCommit' {
                    initiatePlatformEventPublishAfterCommit();
                    initiateHTTPRequest();
                }

                when 'peImmediately' {
                    initiatePlatformEventPublishImmediately();
                    initiateHTTPRequest();
                }
            }
        } catch (Exception objException){
            strMessage = objException.getMessage();
        }

        return strMessage;
    }

    private static void initiatePlatformEventPublishAfterCommit() {
        EventBus.publish(new PEPublishAfterCommit__e());
    }

    private static void initiatePlatformEventPublishImmediately() {
        EventBus.publish(new PEPublishImmediately__e());
    }


    private static void initiateHTTPRequest() {
        HttpRequest httpRequest = new HttpRequest();
        httpRequest.setEndpoint('https://jsonplaceholder.typicode.com/todos/1');
        httpRequest.setMethod('GET');
        new Http().send(httpRequest);
    }

    @Future
    private static void initiateFutureJob() {
        System.debug('Future Job');
    }

    private static void initiateBatchJob() {
        Database.executeBatch(new BatchInsertAccount());
    }

    private static void initiateQueueableJob() {
        System.enqueueJob(new QueueableInsertAccount());
    }

    public class QueueableInsertAccount implements Queueable {
        public void execute(QueueableContext context) {
            Id recordTypeId = Account.getSObjectType().getDescribe().getRecordTypeInfosByName().get('CustomRT').getRecordTypeId();
            insert new Account(Name = 'TestUncommittedWorkPending', RecordTypeId = recordTypeId);
        }
    }
}

Lets see the result in a video demo:

Did you find this article valuable?

Support Nagendra Singh by becoming a sponsor. Any amount is appreciated!