Database.Stateful, Non-Static Variable Is Null in Finish Method

apexbatchnullstateful

I can't figure out what I'm doing wrong here. So, I have a batch class, when I call it, it instantiates a new List<Database.SaveResult>() which is non-static and since I'm implementing Database.Stateful I thought that that's all I had to do, but when it gets referenced in the finish() method, it's null!!

My error:

NullPointerException: Attempt to de-reference a null object
BatchablePlacementChecker.finish() line 27

The line that is failing is this (second line of the finish() method):

 if (errors.size() < 1 && !Test.isRunningTest()) {return;}

'errors' is the name of my variable, new List<Database.SaveResult>(); which gets instantiated in the constructor when I call the batch class.

The execute() method adds any Database.SaveResult objects to the errors variable if the save result isn't a success. I am collecting them to email them as a CSV to any currently active System Administrators in the finish() method, just so you have the background.

I'm calling this batch class in my test like this:

Database.executeBatch(new BatchablePlacementChecker(), 50);

Here's the full class:

public without sharing class BatchablePlacementChecker implements Database.Batchable<SObject>, Database.Stateful {
    public List<Database.SaveResult> errors;

    public BatchablePlacementChecker() {
        List<Database.SaveResult> errors = new List<Database.SaveResult>();
    }

    public Database.QueryLocator start(Database.BatchableContext dbc) {
        return Database.getQueryLocator('SELECT Id, On_Placement__c, (SELECT Id FROM Placements__r WHERE (Start_Date__c <= TODAY AND End_Date__c > TODAY) OR Confirmed_Start_Date__c <= TODAY) FROM Candidate__c');
    }

    public void execute(Database.BatchableContext dbc, List<Candidate__c> candidates) {
        List<Candidate__c> processed = new List<Candidate__c>();
        for (Candidate__c candidate : candidates) {
            candidate.On_Placement__c = candidate.Placements__r.size() > 0 ? true : false;
            processed.add(candidate);
        }
        List<Database.SaveResult> results = Database.update(processed, false);
        for (Database.SaveResult result : results) {
            if (!result.isSuccess()) {errors.add(result);}
        }
    }

    public void finish(Database.BatchableContext dbc) {
        sendNotification('Batch Job Completed', 'The BatchablePlacementChecker job has finished.');
        if (errors.size() < 1 && !Test.isRunningTest()) {return;}
        String csvOfIds = 'ID,Errors\n';
        for (Database.SaveResult erroredRecord : errors) {
            csvOfIds += erroredRecord.id + ',' + erroredRecord.errors + '\n';
        }
        Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
        Messaging.EmailFileAttachment attachment = new Messaging.EmailFileAttachment();
        attachment.setFileName('[' + Date.today() + '] BatachablePlacementCheckerErrors.csv');
        attachment.setBody(Blob.valueOf(csvOfIds));
        email.setFileAttachments(new Messaging.EmailFileAttachment[]{attachment});
        String[] toAddresses = new String[]{};
        for (User administrator : [SELECT Id, Email FROM User WHERE Profile.Name = 'System Administrator' AND IsActive = TRUE]) {
            toAddresses.add(administrator.Email);
        }
        email.setToAddresses(new String[]{'[email protected]'}); //Update this to toAddresses for live.
        email.setSubject('Errors During BatchablePlacementChecker'); 
        email.setHtmlBody('Some Candidate records failed to save when running the BatachablePlacementChecker class.');
        if (!Test.isRunningTest()) {Messaging.sendEmail(new Messaging.SingleEmailMessage[] {email});}
    }

    private void sendNotification(String title, String body) {
        CustomNotificationType notificationType = [SELECT Id, DeveloperName FROM CustomNotificationType WHERE DeveloperName = 'Batch_Job'];
        Messaging.CustomNotification notification = new Messaging.CustomNotification();
        notification.setTitle(title);
        notification.setBody(body);
        notification.setNotificationTypeId(notificationType.Id);
        notification.setTargetId(UserInfo.getUserId());
        try {notification.send(new Set<String>{UserInfo.getUserId()});}
        catch (Exception e) {System.debug('Problem sending notification: ' + e.getMessage());}
    }
}

Best Answer

The problem is the datatype declared in the constructor.

public List<Database.SaveResult> errors;

public BatchablePlacementChecker() {
    List<Database.SaveResult> errors = new List<Database.SaveResult>();
}

This is what we refer to as Name Shadowing. Basically, you created a local variable that overrode the instance variable by the same name. This isn't unique to constructors; you can do so in any method (constructors are just a special type of method).