5

When using a custom Iterable/Iterator class with Database.Batchable, if the Iterator throws an exception it seems impossible to catch the exception.

Is there any way to catch the exceptions thrown in the following example code?

public class BatchableWithTryCatch implements Database.Batchable<String> {
    public Iterable<String> start(Database.BatchableContext bc) {
        System.debug(
            'youll see this message in the same log with the exception'
        );

        try {
            return new ErrorIterator();
        } catch (Exception e) {
            System.debug(
                'this doesnt catch anything because there is no exception ' +
                'thrown by the constructor'
            );
            return null;
        }
    }

    public void execute(Database.BatchableContext bc, List<String> scope) {
        System.debug(
            'execute() doesnt get called because of exception in hasNext()'
        );
    }

    public void finish(Database.BatchableContext bc) {
        System.debug(
            'finish() doesnt get called because of exception in hasNext()'
        );
    }

    public class ErrorIterator implements Iterable<String>, Iterator<String> {
        public Iterator<String> iterator() {
            return this;
        }

        public Boolean hasNext() {
            throw new StringException(
                'Cant catch exceptions from hasNext()'
            );
            return true;
        }

        public String next() {
            throw new StringException(
                'Cant catch exceptions from next()'
            );
            return 'foo';
        }
    }
}

This is what the log looks like for the first 'Batch Apex' execution:

36.0 APEX_CODE,DEBUG;APEX_PROFILING,NONE;CALLOUT,NONE;DB,NONE;SYSTEM,NONE;VALIDATION,NONE;VISUALFORCE,NONE;WORKFLOW,NONE
18:29:46.0 (436218)|EXECUTION_STARTED
18:29:46.0 (475757)|CODE_UNIT_STARTED|[EXTERNAL]|01p36000006TMzQ|BatchableWithTryCatch
18:29:46.0 (17094809)|USER_DEBUG|[3]|DEBUG|youll see this message in the same log with the exception
18:29:46.0 (18531282)|FATAL_ERROR|System.StringException: Cant catch exceptions from hasNext()

Class.BatchableWithTryCatch.ErrorIterator.hasNext: line 36, column 1

It does make sense that the exceptions thrown by the Iterator class are not caught by the try/catch block in the start() method, because those methods are called by Salesforce after calling the start() method in order to set up the scope for the execute() method.

However, it is a major disadvantage not being able to catch these exceptions, so I am hoping that I am just unaware of some kind of existing error handling mechanism!

How can I catch exceptions thrown from a custom Iterable/Iterator class when used with Database.Batchable?

2 Answers 2

4

Basically, problem is that next method is not called in execute method of batch.

Lets make next assumptions -- if we have some custom iterator like this:

global class CustomIterable implements Iterator<Object>{ 
    integer counter;
    public CustomIterable(){ 
        counter = 0;
    }   

    public boolean hasNext(){ 
        return true;//or false
    }

    public Object next(){ 
        return <some Object>;
    }
}

, and batch like this:

public class CustomBatch implements Database.Batchable<object>{

    public Iterable<object> start(Database.batchableContext info){ 
        return new  CustomIterable();
    } 
    public void execute(Database.batchableContext info, List<Account> scope){ 
        //do something iterable
    }
    global void finish(Database.batchableContext info){ 
        //do something as result
    } 

}

Then, pseudocode that do real job in salesforce, would look like next:

class Database{
    public static executeBatch(Database.Batchable<object> batch_instance){
        Database.batchableContext info = buildContext();//some internal method
        Iterable<sObject> custom_iterator = batch_instance.start(info);
        while(custom_iterator.hasNext()){
            batch_instance.execute(info, custom_iterator.next());
        }
        batch_instance.finish(info);
    }
}

Of course, real code is more complex to handle limits, Those approach was described here and/or here So, basically, what you can do - you can iterate on some custom class objects, like:

class SingleBatchWrapper{..}

, which would contain list of actual records, that you really need to process, and some extra field, like error, timestamp etc, so:

class SingleBatchWrapper{
    List<Object> RealData;
    Boolean IsSuccessful;
    String ErrorMessage;
}

Then, in case of building your custom iterator, you can have it like:

global class CustomIterable implements Iterator<SingleBatchWrapper>{ 
    integer counter;
    SingleBatchWrapper NextBatch;
    public CustomIterable(){ 
        counter = 0;
    }   

    public boolean hasNext(){ 
        // set Next batch.
        try{
           NextBatch = buildNextBatch();
        }catch(Exception ex){
           NextBatch = buildFailedBatch(ex);
        }
        return true;//or false if there are no more records
    }

    public Object next(){ 
        return NextBatch;
    }
}

So, in that case, you can check your SingleBatchWrapper data in your execute method like:

public class CustomBatch implements Database.Batchable<SingleBatchWrapper>{
    //some extra methods
    public void execute(Database.batchableContext info, List<SingleBatchWrapper> scope){ 
        for(SingleBatchWrapper wrapper : scope){
            if (wrapper.isSuccessful){
            //catch good things
           }else{
           //catch everything else
           } 
        }
    }
}

So, as a result, you would move your exceptions thrown inside execute method, and would be able to process it there with more clean way -- for example, stop batch or notify user about issues in the finish method by using Database.stateful interface if needed.

P.S. Code above are pseudocode, that describes approach, not actual solution and was written in notepad, so it may have typos

2
0

To extend @kurunve excellent answer but applied to processing CSV files in a batchable.

Validations to do in constructor of the iterable

You can throw exceptions that can be caught in your batchable start()

  • CSV file exists
  • CSV file is not too large to blow up heap or other application limit
  • CSV file header is as expected

Validations to do in the iterator's next() method

The iterator's next() method should read one CSV row starting after the header (which was read in the iterable's constructor)

You can not throw exceptions here at all as they won't be caught by any try/catch in either batchable start(), execute(), or finish()

  • CSV row has legit data

As kurunve states, your batchable start should either be

  • an Iterable<Object> of some customType with at least a Boolean property isInError that your execute() can inspect and ignore the "record" passed in the scope argument
  • an Iterable<SObject> where you use some custom field Is_In_Error__c (or maybe the Status field available on some OOTB objects) that your execute() can inspect and ignore the sObject passed in the scope argument collection.

This is quasi-code outlining the approach for SObjects

public class CsvRowIterable implements Iterable<SObject>, Iterator<SObject> {
    
    String body;            // CSV body as uploaded
    CsvReader csvRdr;       // Our CSV parser
    String[] csvRow;        // CSV row, split into cells
    Integer rowNum;         // Current row Number in CSV, starts counting at 1

    /**
     * Constructor does validation that rqst is legit and CSV file has proper header. Any exceptions thrown back to batchable start()
     */
    public CsvRowIterable(Id contentDocumentId) {
    
        this.body = [SELECT Id, VersionData
                      FROM ContentVersion
                      WHERE ContentDocumentId = :contentDocumentId][0].VersionData.toString();
        this.csvRdr = new CsvReader(this.body); // your favorite CSV reader here
        this.csvRow = this.csvRdr.readLine();   // get header row
        // !! validate CSV header here; exceptions can be thrown/caught
        this.rowNum = 1;

    }
    /**
     * No exceptions can be thrown in iterator as they can't be caught by either batchable start() or batchable execute()
     */
    public Iterator<SObject> iterator() {       // returns ourself that implements hasNext() and next(); returning one Order on the next() call
        return this;
    }

    public Boolean hasNext() {
        this.csvRow = csvRdr.readLine();
        return this.csvRow != null;
    }
    public Order next() {
        this.rowNum ++;
        return toOrder(this.csvRow);
    }
    Order toOrder(String[] csvRowCells) {
       return isValidRow(csvRowCells)
         ? new Order(fld1 = 'x', ..., Status = 'Draft')
         : new Order(Status = 'Error'); 

    Boolean isValidRow(String[] csvRowCells) {
     // do csv cell validation here
    }
}

and your batchable start() looks like this:

public Iterable<SObject> start(Database.BatchableContext bc) {
    
    try {
        return new CsvRowIterable(someContentDocumentId);
    }
    catch (Exception e){
        return new List<SObject>();
    }
}

You must log in to answer this question.

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