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.
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
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:
If in Anonymous Apex you should do
You get back
If you but reverse the order of the callout and email send, all is well:
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.