8

Salesforce QueryLocators have a number of restrictions, including that a user can only have 50 active at a time, and that they time out after about 15 minutes of inactivity.

I'm trying to debug some code that is running into those restrictions, but can't figure out how to catch the exception. Here's a sample:

public PageReference refreshWhichFails () {
    System.debug('Method starting.');
    try {
        callThatFailsWithInvalidQueryLocator();
    } catch (Exception e) {
        System.debug('Oops, something went wrong!');
    }
    System.debug('Method ending.');
}

To test this, after loading my page I run an execute anonymous in which I instantiate 50 QueryLocators, which invalidates the QueryLocation in my VF page. Then I run this method, and get a fatal error:

This page uses a QueryLocator that is invalid. There is a limit of 50 QueryLocators per user.

When I inspect my debug logs, only the first debug statement is there, and execution fails during callThatFailsWithInvalidQueryLocator.

How can I catch invalid QueryLocator errors?

  • What's the query that's getting the invalid query locator error? Failing the ability to catch the query locator error there are tricks to avoid having them expire on you (i.e. avoiding subqueries). – Ralph Callaway Nov 3 '13 at 23:36
  • I'm 99% sure that the issue in this case isn't subqueries, and is simply a timeout. We have a search page where, for various reasons, users may finish their work within 2-3 minutes, but may also get interrupted and want to smoothly resume after 15-20 min -- and these are the situations causing errors. My hope would be to notice and catch when the QueryLocator has expired, and then reinitialize it (and set the StandardSetController to the correct page, etc.) smoothly within the same request. – Benj Nov 4 '13 at 13:35
  • Totally, just seems like that's not going to happen. If you post your initialization for the StandardSetController I might have some other suggestions. – Ralph Callaway Nov 4 '13 at 17:20
10

Given your code above and the result, this sounds like an "exception that cannot be caught" (see the end of this page), like a limit exception. Unfortunately, I'm not aware of a definitive list of such exceptions.

It isn't elegant, but perhaps you could track the age of the QL in a Datetime, and reinitialize it any time it's over 10 minutes old?

Update 22 Jan 2016: So I ran into this error today; imagine my surprise when I searched SFSE and found that I'd been the one to answer the question! I had built a page using a private StandardSetController + cached wrapper objects to manage pagination, and twice during the day I stopped for a meeting, receiving the error upon returning to the page and trying to navigate. Certainly looked like a timeout.

Since I had already added controller methods to call the standardSetController methods, e.g.:

public void nextPage() {
    setCon.nextPage();
}

I added a private member and method to manage the timeout:

// initialized to DateTime.now() when setCon is initiallized
private setControllerBirthday {get; set;} 

// Make SetController safe 
private void BubblewrapSetController() {
    if (setControllerBirthday < Datetime.now().addMinutes(-10)) {
        integer pageNum = setCon.getPageNumber();  //maintain current page
        integer pageSize = setCon.getPageSize();   //maintain current records per/page setting
        id filterId = setCon.getFilterId();        //maintain current filter
        setCon = new ApexPages.StandardSetController(Database.getQueryLocator([...]));
        setCon.setPageSize(pageSize);
        if (filter != null && filter != '000000000000000AAA') {
            setCon.setFilterId(filter);            //restore filter if in use
        }
        setCon.setPageNumber(pageNum);             //restore page
        setControllerBirthday = DateTime.now();    //restart the clock
    }
}

And finally, I updated the navigation methods to call the new method, e.g.,

public void nextPage() {
    BubblewrapSetController();
    setCon.nextPage();
}

This works as expected, even after 20 minutes of inactivity. Combined with my wrapper object caching, it provides a seamless editing experience without risk of timeouts.

Update 30 May 2019: I've made a small change to BubblewrapSetController; I now check that the value of the filter id returned by setCon.getFilterId() is not null and not '000000000000000AAA' before assigning it to the new set controller's filterId. It seems that if you were not explicitly using a filter in the original set controller, setting it to '000000000000000AAA' (which is SF ID encoding of 0, and which is what getFilterId() returns when no filter is set) causes the set controller to "reset" the filter, effectively dropping the where clause from the query locator. I'm not sure when this change occurred, but validating the filter value before using it addresses the issue.

Update 31 Jul 2019: Added a check & restore of pageSize, which is the number of records per page; if you set this initially it will be lost when the bubblewrap method replaces the set controller (just like pageNumber).

  • Nice methodology. That "birthday" should probably be private set though, no? Also it is 2016! :P – Adrian Larson Jan 22 '16 at 20:58
  • Actually, the whole thing is private, since it is only used in internal the bubblewrap() method. And, fixed the date, thx! :D – Jason Clark Jan 22 '16 at 21:00
  • Ah didn't see that it was a private member. Nice work, +1. – Adrian Larson Jan 22 '16 at 21:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.