18

I'm having an issue where we have some code that can be called anywhere and then it makes an @future call to log a message to an external system.

The problem is if this method is called (either directly or indirectly) in a Controller Extension a System.LimitException is thrown saying @future call currently not allowed

I know you can check to see the number of allowed future calls with Limits.getLimitFutureCalls() and I know you can see how many you've made with Limits.getFutureCalls() and see if it's batch with with System.isBatch() or is in future call with System.isFuture(). Is there any other way to know if you cannot make a future call?

My goal would be to check to see if the @future call can be made, and if not skip the method call.

NOTE: You cannot make @future calls from get/set/constructor in the Controller extension and metioned in the documentation

Example of bad code

Class

public class GenericUtils {
    public static void reportException(Exception e) {
        //Limits.getFutureCalls() returns 0
        //Limits.getLimitFutureCalls() return 10

        //Make @future call to log
        System.debug(e.getMessage());
    }
}

Controller

public with sharing class ControllerExtension {
    public ControllerExtension(ApexPages.StandardController cont) {
        //Do stuff
    }

    public getMyVariable() {
        try {
            //Do more stuff
        } catch (Exception e) {
            GenericUtils.reportException(e); //This throws the LimitException
        }
    }
}

Page

<apex:page standardController="Account" extensions="ControllerExtension">
    <apex:form id="theForm">
        <apex:inputText value="{!myVariable}" />
    </apex:form>
</apex:page>
12
  • 1
    LimitExceptions cannot be handled , so using the Limit methods is prolly the only reliable way. As an aside, future methods are okay in VF controllers save for get/set and constructors - like DML Operations. Apr 16, 2013 at 22:03
  • They are not ok in the get/set/constructors as seen in the documentation Apr 16, 2013 at 22:08
  • Per out chat on IRC, did you check if Limits.getLimitFutureCalls() returns 0 in the getter/setter scenario mentioned? Apr 17, 2013 at 2:34
  • 1
    As an aside, I've always programmed getters/setters to be simple and if something more complex (e.g. complex calculation, db call) was required created separate methods. If your getter can raise an exception you may want consider refactoring or changing the architecture.
    – Mike Chale
    Apr 17, 2013 at 14:09
  • 2
    Example: stackoverflow.com/questions/1488472/… (Note: I come from a .Net background, YMMV)
    – Mike Chale
    Apr 17, 2013 at 14:09

4 Answers 4

11
+50

Answer and Why. After spending some further time on this, I believe the answer to your question in determining if the future feature is available programatically is unfortunately, no this is not possible. I've considered the following in arriving at this answer for you.

  • Catching the LimitException. This is unfortunately not possible. I have had luck in using a ApexPage.getContent callout to catch unhanded exceptions in the past. But this was more relating to the ability to send emails and was not context to the call stack (e.g. in a get method).
  • Parsing the Stack Trace from a freshly created Exception instance. Unfortuantly this is not reliable enough information. Not only is it not generally available (once your code is running in a subscriber org), it would require some maintenance of the detection logic.
  • Utilising the Limit Methods. These are request scope in their responses, it could be argued that Limits.getLimitFutureCalls() should return 0 if called from a call stack that containts a VF get/set or constructor, but sadly they don't presently. Nor do I see Salesforce changing this anytime soon.

Alternative thoughts to your requirement...

There is plenty of architecture best practice and guidelines that tell the developer about when this feature is not allowed. However this does not help the developer providing a utility function aimed at being called from various contexts. This does present difficulties in implementing a generic logging solution such as yours. There is also the fact that, as you've discovered some exceptions cannot be caught by Apex code and thus will bypass your logging utility function completely.

  • Apex Exception User for Packages. . For those unhandled exceptions (or indeed exceptions you don't care to catch deliberately) the platform will automatically email you the provider of the package with the details, including stack dumps as described here. You might also find this cookbook article useful, in conjunction with this approach you could route such emails to your external service perhaps?

  • Scheduled Job and Staging Errors in a Custom Object. You can of course catch exceptions as you've discovered. It might be a better route to consider then a Schedule Apex job that collects unsent errors and sends them to your external web service. This will resolve issues where you cannot make further future calls for example once already in a future call or batch execute. Unfortunatly you still cannot make DML calls in get/set methods however, so any persistant type of logging is not going to be possible in these contexts. If you go down this route consider this.

Other Recommendations

I do recommend that you consider refactoring some of your complex logic from get/set methods and into action methods where you logging will be more manageable (as per this answer). You may have to accept that 100% of exceptions cannot be caught, in which case these will surface via the Apex Exception email back to you from your subscriber orgs.

2
  • I agree with all of this. I've not been able to find any way to do this reliably. Even a support ticket with SFDC has resulted in an answer of "we do not have a programmatic way to determine this." I have created an Idea to help track this request. May 14, 2013 at 17:34
  • Salesforce has added in a QUIDDITY enum which can be retrieve at runtime. Using this, you can get the current transaction's QUIDDITY and see if the current transaction is of a type where a @future cannot be executed, like Batch apex.
    – Zerkz
    Aug 10, 2021 at 20:46
4

This is going to be an extremely stupid answer but here goes...

So you kind of want to know whether you're in a getter/setter/constructor context? We'll be building on my embarrassing idea to throw an exception, catch it and thoroughly abuse the stacktrace.

Here's the sample class:

public with sharing class someController {
    public List<Account> getAccountsNormalGetter(){
        System.debug(log());
        return [SELECT Id, Name FROM Account LIMIT 5];
    }

    public List<Opportunity> opportunitiesPropertyGetter {get{
        System.debug(log());
        return [SELECT Id, Name FROM Opportunity LIMIT 5];
    } set;}

    public someController(){
        System.debug(log());
    }

    public PageReference normalActionMethod(){
        System.debug(log());
        return null;
    }

    public void anotherNormalActionMethod(){
        System.debug(log());
    }

    public Integer normalMethod(){
        System.debug(log());
        return 5;
    }

    public static String log(){
        try{
            throw new LoggerException();
        } catch (LoggerException le){
            String stackTrace = le.getStackTraceString();
            //System.debug(LoggingLevel.ERROR, stackTrace);
            String lastLine =  stackTrace.split('\n')[1];
            return lastLine.left(lastLine.indexOf(':'));
        }
    }

    public class LoggerException extends Exception{}
}

Here's the exec. anonymous to test it:

someController ctrl = new someController();
List<Account> accs = ctrl.getAccountsNormalGetter();
List<Opportunity> opps = ctrl.opportunitiesPropertyGetter;
ctrl.normalActionMethod();
ctrl.anotherNormalActionMethod();
ctrl.normalMethod();

And the relevant lines of the debug log look like this:

USER_DEBUG|[13]|DEBUG|Class.someController.<init>
USER_DEBUG|[3]|DEBUG|Class.someController
USER_DEBUG|[8]|DEBUG|Class.someController.__sfdc_opportunitiesPropertyGetter
USER_DEBUG|[17]|DEBUG|Class.someController.normalActionMethod
USER_DEBUG|[22]|DEBUG|Class.someController.anotherNormalActionMethod
USER_DEBUG|[26]|DEBUG|Class.someController.normalMethod

So looks like you can just add 1 more method to this abomination and you're on your way.

List<String> entryPoints = new List<String>{'Class.someController.<init>', 'Class.someController', 
    'Class.someController.__sfdc_opportunitiesPropertyGetter', 'Class.someController.normalActionMethod', 
    'Class.someController.anotherNormalActionMethod','Class.someController.normalMethod'
};

for(String s : entryPoints){
    List<String> pieces = s.split('\\.');
    if(System.isBatch() || System.isFuture() || pieces.size() == 2 || (pieces.size() > 2 && (pieces[2] == '<init>' || pieces[2].startsWith('__') || pieces[1] == pieces[2]))){
        System.debug(s + ' => not safe');
    } else {
        System.debug(s + ' =>  safe');
    }
}
3
  • Hrm, this is an interesting approach, I am going to look into this. May 9, 2013 at 17:52
  • If it will be called from some utility classes and not always from the top controller you'll have to play with stack depth (sometimes take line 2,3,... not line 1). Or other way around, always look at "second-to-last" for example (then again I suspect exec. anonymous blocks will yield different results than normal code usage).
    – eyescream
    May 9, 2013 at 18:11
  • It does appear differently. In my output, I do not actually see the call to the getter method. The only parts that show up in the stack trace are the main controller, and the secondary utils method call. If I blocked all calls from controller classes then I could be missing a lot of valid calls outside the constructor/getter/setters May 9, 2013 at 18:29
2

I don't think there's a way to ask if an @future call is permitted. But as you've pointed out, you can ask whether a thread is coming from a future or batch process to avoid calling @future.

But @future calls are in fact allowed in a controller and also in controller extensions. The following will work in either:

Page

<apex:page standardController="Account" extensions="AccountStandardController">
<apex:sectionHeader title="Standard Controller" subtitle="Account"/>
    <apex:form id="theForm">
        <apex:commandButton value="Future" action="{!doFuture}"/>
    </apex:form>
</apex:page>

Apex

public with sharing class AccountStandardController 
{
    public AccountStandardController(ApexPages.StandardController controller){}

    public PageReference doFuture()
    {
        doTheFuture();
        return null;
    }

    @Future
    public static void doTheFuture()
    {
    }   
}

If you're still having trouble, post your code so we can help.

1
  • The problem is in the get, set or constructor. I'll update the question tomorrow when I have some of the code in front of me. Apr 17, 2013 at 1:36
2

You can use Quiddity enum to work out the context, for example:

Quiddity quiddity = Request.getCurrent().getQuiddity();
if (quiddity == Quiddity.VF || quiddity == Quiddity.BATCH_APEX) {
    futureSendEmailToDev(emailSubject, emailBody);
} else {
    sendEmailToDev(emailSubject, emailBody);
}

You must log in to answer this question.

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