OK, for those having trouble getting this to work, here's my set-up. I've included stevil's fix too, for good measure.

PHP Code:
Ext.onReady(function(){

    
Ext.define('User', {
        
extend'Ext.data.Model',
        
fields: ['id','name','balance'],
        
idProperty'id'
    
});

    var 
store Ext.create('Ext.data.Store', {
        
model'User',
        
pageSize60,
        
bufferedtrue,
        
remoteSorttrue,
        
purgePageCount5,
        
proxy: {
            
type'ajax',
            
url'yourURLHere',
            
reader: {
                
type'json',
                
root'data',
                
totalProperty'total'
            
},
            
simpleSortModetrue
        
}
    });

    var 
grid Ext.create('Ext.grid.Panel', {
        
id'myGrid',
        
storestore,
        
verticalScroller: {
            
xtype'paginggridscroller',
            
activePrefetchtrue // preload new set just before you reach the end
        
},
        
invalidateScrollerOnRefreshfalse,
        
columns: [
            {
                
dataIndex'id',
                
header'ID',
                
width60,
                
flex:6
            
},{
                
dataIndex'name',
                
header'Name',
                
width150,
                
flex:15
            
},{
                
dataIndex'balance',
                
header'Balance',
                
format'$0,000.00',
                
width80,
                
flex:8
            
}
        ]
    });

    var 
vp Ext.create('Ext.container.Viewport', {
        
renderToExt.getBody(),
        
layout'fit',
        
items: [
            {
                
title'Infinite Scroll Test',
                
layout'fit',
                
items: [grid]
            }
        ]
    });

    
vp.show();
    
store.guaranteeRange(0store.pageSize 1);


    
// probably not necessary in real-life situations, but just in case...
    // add this override to handle empty data
    
Ext.override(Ext.data.Store, {
        
onGuaranteedRange: function() {
            var 
me this,
                
totalCount me.getTotalCount(),
                
start me.requestStart,
                
end = ((totalCount 1) < me.requestEnd) ? totalCount me.requestEnd,
                
range = [],
                
record,
                
start;

            if (
start end) {
                
me.guaranteedStart start;
                
me.guaranteedEnd undefined;
                
me.totalCount undefined;
            }
            else if (
start !== me.guaranteedStart && end !== me.guaranteedEnd) {
                
me.guaranteedStart start;
                
me.guaranteedEnd end;

                for (; 
<= endi++) {
                    
record me.prefetchData.getByKey(i);

                    
//should never happen
                    
if (!record) {
                        
Ext.Error.raise("Record was not found and store said it was guaranteed");
                    }

                    
range.push(record);
                }
                
me.fireEvent('guaranteedrange'rangestartend);
                if (
me.cb) {
                    
me.cb.call(me.scope || merange);
                }
            }

            
me.unmask();
        }
    });
    
    
Ext.Error.handle = function (err) {
        if (
err.sourceMethod == "onGuaranteedRange") {
            return 
true;
        }
    };  
}); 
As long as your data is formatted properly, you should NEVER get either of those pesky errors.

You should grab a data set based on the start and limit params that are automatically sent, and the response should look something like this: {"data":[{"id":"1","name":"Foo Bar","balance":500},...],"total":<the total of ALL the data>}

The grid will load a new set just before you scroll to the end of each page. The last page will contain a smaller set of data, but it loads without error.

Hope this helps!