13

We need to implement the following pattern at my org:

  • callout to external data source
  • if that callout takes too long (according to some configurable threshold), log an error (ie do some DML)
  • if that callout timed out on the remote server, try it again

Recognizing the potential for the dreaded "You have uncommitted work pending. Please commit or rollback before calling out." error, I put the error logging code in a future method, thus isolating the DML from the callouts. However, the error is still being thrown. I reduced the issue down to this pattern:

public static void foo() {
    Http http = new Http();
    HttpRequest req = new Httprequest();
    req.setEndpoint('https://test.salesforce.com'); //whatever endpoint
    req.setMethod('GET');
    http.send(req); //works fine
    bar();
    http.send(req); //throws calloutexception
}

@future public static void bar() {

}

Am I correct to assume that calling a future method counts as a DML operation? Is there any documentation I'm missing somewhere?

Here is the log for the above code snippet:

Error on line 8, column 1: System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out
AnonymousBlock: line 8, column 1
41.0 APEX_CODE,DEBUG;APEX_PROFILING,INFO;CALLOUT,INFO;DB,INFO;SYSTEM,DEBUG;VALIDATION,INFO;VISUALFORCE,INFO;WAVE,INFO;WORKFLOW,INFO
12:41:06.606 (606820392)|UNKNOWN|[EXTERNAL]
12:41:06.606 (606893134)|EXECUTION_STARTED
12:41:06.606 (606904404)|CODE_UNIT_STARTED|[EXTERNAL]|execute_anonymous_apex
12:41:06.606 (608097326)|SYSTEM_MODE_ENTER|false
12:41:06.606 (608671843)|CALLOUT_REQUEST|[6]|System.HttpRequest[Endpoint=https://test.salesforce.com, Method=GET]
12:41:06.606 (737558005)|CALLOUT_RESPONSE|[6]|System.HttpResponse[Status=OK, StatusCode=200]
12:41:06.606 (862244488)|EXCEPTION_THROWN|[8]|System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out
12:41:06.606 (862467817)|SYSTEM_MODE_EXIT|false
12:41:06.606 (862668955)|FATAL_ERROR|System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out

AnonymousBlock: line 8, column 1
12:41:06.862 (862819672)|CUMULATIVE_LIMIT_USAGE
12:41:06.862 (862819672)|LIMIT_USAGE_FOR_NS|(default)
  Number of SOQL queries: 0 out of 100
  Number of query rows: 0 out of 50000
  Number of SOSL queries: 0 out of 20
  Number of DML statements: 0 out of 150
  Number of DML rows: 0 out of 10000
  Maximum CPU time: 0 out of 10000
  Maximum heap size: 0 out of 6000000
  Number of callouts: 1 out of 100
  Number of Email Invocations: 0 out of 10
  Number of future calls: 1 out of 50
  Number of queueable jobs added to the queue: 0 out of 50
  Number of Mobile Apex push calls: 0 out of 10

12:41:06.862 (862819672)|CUMULATIVE_LIMIT_USAGE_END
12:41:06.606 (862919688)|CODE_UNIT_FINISHED|execute_anonymous_apex
12:41:06.606 (864947070)|EXECUTION_FINISHED

I have also posted this question on the developer forums.

1
  • FWIW: this pattern is indeed not allowed. There are many possible workarounds; out of time constraints/laziness I just decided to set a bool in between callouts, then do DML after the retry based on that flag. Commented Jan 3, 2018 at 13:18

2 Answers 2

19

Using System.enqueueJob, Database.executeBatch, System.scheduleBatch, System.enqueueJob, and @future methods all modify the state of the database, so for purposes of callouts, count as a DML operation. This also means that Database.rollback can undo a scheduled job, batch job, queueable, or future method. Also note that any of those methods will prevent you from using PageReference.getContent, which counts as a callout, as well as web service methods from imported WSDL classes.


Example

public class q203304 {
    @future public static void x() {

    }
}

Execute Anonymous

q203304.x();
Blob c = new PageReference('https://www.google.com/').getContent();

Output:

Line: 2, Column: 1 System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out

3
  • Bummer. Is this documented anywhere? Wasted a ton of time trying to track down a DML operation that didn't exist in my code. Commented Jan 2, 2018 at 22:08
  • @JimMadrigal If it does, I've never seen it. Also, some cursory googling didn't turn up anything resembling official documentation, either. This is one of those things that you tend to find out over time.
    – sfdcfox
    Commented Jan 2, 2018 at 22:34
  • I had a very similar issue, and yeah, what a pain! The CalloutException message doesn't specifically say "DML", but rather "uncommited work pending". I never did get a reference to documentation either, but it's hard to argue with @sfdcfox on the state of the database. Commented Jan 3, 2018 at 1:10
7

It doesn't count as DML, however, queued asynchronous actions enter a queue to be executed after the present transaction. If the Apex transaction rolls back, so does the queue.

You could use try/catch to actually catch the callout exception and then do your DML afterwards to record what the outcome was.

If what you really want is to log something before or between callouts, that remains logged even if you reach an uncaught exception, look into Platform Events as a way to initiate this. Platform Events do NOT roll back, and you can hang triggers or Process Builders off of them to ensure some log is created.

See Andrew Fawcett's Dreamforce talk on this topic: https://www.youtube.com/watch?v=yYeurYnasVc

1
  • 1
    N.B. Platform Events technically do roll back in the AllOrNone=false retry use case where 1+ recs are successes and 1+ recs are fails. But, when the transaction is retried to completion, they don't roll back. As such, that shouldn't affect the use of Platform Events for the OP issue.
    – cropredy
    Commented Jan 2, 2018 at 18:52

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .