0

I've got a trigger that was working perfectly in my sandbox but once pushed into production we receive:

Update_DUA_Last_Activity_Date: execution of AfterInsert

caused by: System.NullPointerException: Attempt to de-reference a null object

Trigger.Update_DUA_Last_Activity_Date: line 11, column 1

L11C1 is the line beginning: if (!t.Subject.startsWith(subjectToExclude) Any ideas where something could have gone wrong in prod?

Trigger in question:

trigger Update_DUA_Last_Activity_Date on Task (after insert, after update) {

    //Do not consider tasks with this subject
    String subjectToExclude = '[Outreach] [Email] [Opened]';
    //Only consider tasks that are related to leads
    String leadPrefix = Lead.sObjectType.getDescribe().getKeyPrefix();

    //Find leads for tasks in trigger
    Set<Id> taskLeadIds = new Set<Id>();
    for (Task t : trigger.new){
        if (!t.Subject.startsWith(subjectToExclude) && String.valueOf(t.WhoId).startsWith(leadPrefix)){
            taskLeadIds.add(t.WhoId);   
        }
    }

    //Query using WhoIds to get lead records
    List<Lead> taskLeads = [SELECT Id, rWeb_Domain__c, LastActivityDate 
                            FROM Lead WHERE Id IN :taskLeadIds];

    //Account for possibility of multiple leads with different LastActivityDate
    Map<String, List<Date>> rWebDateMap = new Map<String, List<Date>>();
    for (Lead lead : taskLeads) {
        if (!rWebDateMap.keyset().contains(lead.rWeb_Domain__c)) {
            rWebDateMap.put(lead.rWeb_Domain__c,new List<Date>());
            rWebDateMap.get(lead.rWeb_Domain__c).add(lead.LastActivityDate);
        } else {
            rWebDateMap.get(lead.rWeb_Domain__c).add(lead.LastActivityDate);
        }
    }

    //Find affected DUAs
    List<Domain_User_Assignment__c> duas = [SELECT Id, rWeb_Domain__c, Last_Activity_Date__c
                                            FROM Domain_User_Assignment__c
                                            WHERE rWeb_Domain__c
                                            IN :rWebDateMap.keyset()];

    //Pick most recent/furthest future Last Activity Date
    for (Domain_User_Assignment__c dua : duas) {
        List<Date> leadDates = rWebDateMap.get(dua.rWeb_Domain__c);
        leadDates.sort(); //Sorts ascending
        dua.Last_Activity_Date__c = leadDates[leadDates.size()-1]; //Use last index
    }

    update duas;

}
1
  • t.whoId != null && t.whoId.getSobjectType() == Lead.SobjectType is better form than the string startsWith approach Commented Dec 7, 2017 at 5:16

1 Answer 1

2

Sounds to me like you only did happy-path testing here.

Rooting out issues like these often comes down to an exercise of deducing and checking which expressions in the offending line can be null and then try to do something with that null value.

Based on your code, t can never be null, and Subject is a required field on Task, so that can't be the cause either.

My guess would be that your t.whoId is the culprit here.

Running the following snippet in anoymous apex confirms that this could indeed be your issue:

// If declared, but not initialized, the value of a variable will be null
Id someId;
// String.valueOf(<uninitialized variable>) seems to itself return null
// null.contains() = NPE
System.debug(String.valueOf(someId).contains('test'));

Avoiding this issue in the future calls for writing unit tests off of the "happy path". The "happy path" is where all inputs that you expect to use are specified and well-formed. The "unhappy path", "sad path", or "adverse path" consists of every situation where you are either missing some input, or the input is malformed. Testing the "adverse path" helps to make your code more robust (i.e. able to gracefully handle adverse situations like this), and is something I would suggest you strive to do. I'd even put "adverse path" testing ahead of getting sufficient coverage (because testing enough adverse paths + the happy path will naturally lead to high coverage).

In this case, your malformed input is a Task without a whoId.

Solving the issue in code is easy, you just need to add a null check before you actually use t.whoId. This is also an opportunity to specify exactly what will happen in this situation.

for (Task t : trigger.new){
    // In this particular case, it probably makes sense to just ignore the task
    //   if it doesn't have a WhoId (rather than throwing an exception).
    // A simple null check at the beginning of this if statement to short-circuit the
    //   evaluation should do fine.
    // Note that you should also look for other instances in your trigger where you
    //   assume that t.WhoId is not null.
    if (t.WhoId != null && !t.Subject.startsWith(subjectToExclude) && String.valueOf(t.WhoId).startsWith(leadPrefix)){
        taskLeadIds.add(t.WhoId);   
    }
}
3
  • As soon as I posted the question I was thinking I should probably wrap everything after the population of taskLeadIds with if (taskLeadIds.size() > 0){ ... }. Sounds like I was on the right path but you gave an excellent explanation. Will do some testing now. Commented Dec 6, 2017 at 20:12
  • @JaredT I'd actually say that wrapping the rest of your code in if(taskLeadIds.size() > 0) is not strictly necessary here. This set being empty isn't an issue for your taskLeads query (the query will just return 0 records), and since the set is initialized (and thus never null), subsequent uses of that set would cause no problems. Your other code, further down in the trigger, seems to be able to safely handle situations where you don't have any taskLeads. Commented Dec 6, 2017 at 20:31
  • @JaredT That said, I tend to err on the side of being more explicit (having this check instead of just assuming things will go fine without the check). If you do include a check like this, I would suggest returning early (going with the principle of failing early/fast) instead of wrapping the rest of your code. E.g. if(taskLeadIds.isEmpty()){return;} /* other code */ instead of if(taskLeadIds.size() > 0){ /* other code */} Commented Dec 6, 2017 at 20:32

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.