[SalesForce] Confused by CalloutException

I have a trigger on Account and Opportunity – both do a callout to the Google Geocoding API and then update itself using a future method. Now I also want to send an email before the update if certain criteria are met – and suddenly all my unit tests fail.

System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out

I'm super confused as to why this happens when all I want is to send an email. Any ideas?

Also, it should be noted that this is not a duplicate of System.CalloutException: You have uncommitted work pending as my error only occurs when I'm trying to send an email. So my question is specifically why it only occurs when I add that email sending code.

Best Answer

Some operations are "DML-ish", meaning they persist something to the database to be committed at the end of the transaction, just not a standard DML operation on an sObject. Enqueuing Batch Apex, for example, is a DML-ish operation. (This is a term I made up, by the way; I don't know if there's official terminology to describe this type of operation).

It's documented that

[Outbound] email is not sent until the Apex transaction is committed.

Because this is persisting something (the email send attempt) until the transaction commits, it has the same effect on later callouts as regular DML - that is, it blocks them due to the uncommitted work that's in flight.

The key is ordering - ensuring that all your callouts happen first, followed by all database mutation.

Here's a simple demonstration. Note that this is not in test context, where we can't send outbound email anyway. Given this class:

public class TestQ240040 {
    public static void runTest() {
        Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();

        email.setToAddresses(new List<String>{'david@ktema.org'});
        email.setPlainTextBody('Text');
        email.setSaveAsActivity(false);
        Messaging.sendEmail(
            new List<Messaging.Email> {email}
        );

        // Now make a callout
        Http http = new Http();
        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://www3.septa.org/hackathon/Arrivals/Market%20East/100');
        req.setMethod('GET');

        HttpResponse res = http.send(req);
    }
}

If in Anonymous Apex you should do

TestQ240040.runTest();

You get back

System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out

If you but reverse the order of the callout and email send, all is well:

public class TestQ240040 {
    public static void runTest() {
        Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();

        email.setToAddresses(new List<String>{'david@ktema.org'});
        email.setPlainTextBody('Text');
        email.setSaveAsActivity(false);

        // Now make a callout
        Http http = new Http();
        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://www3.septa.org/hackathon/Arrivals/Market%20East/100');
        req.setMethod('GET');

        HttpResponse res = http.send(req);

        Messaging.sendEmail(
            new List<Messaging.Email> {email}
        );
    }
}

No exception, and the email gets delivered as expected.

Now, I would actually expect a different error from your unit tests (since you cannot send outbound email there), but I think the above is the core issue leading to the exception you're discussing here.

Related Topic