Why is “Record is read-only” Error Occurring?

When working with an after insert trigger in Salesforce, attempting to modify fields on Trigger.new will result in a “System.FinalException: Record is read-only” error. This error occurs because records in Trigger.new are immutable in after context triggers since the DML operation has already been completed.
Here’s an example scenario: You want to update the Product_Category__c field on the OpportunityLineItem object after its creation, based on the Product_Category_new__c field on the related Product. The following trigger causes the error because it tries to directly modify Trigger.new:
trigger SetProductCategory on OpportunityLineItem (after insert) {
    for (OpportunityLineItem opplineitem: Trigger.new) {           
        opplineitem.Product_Category__c = opplineitem.PricebookEntry.Product2.Product_Category_new__c;
    }
}Why is this happening?
In an after insert trigger, Salesforce has already performed the DML operation. Therefore, Trigger.new is read-only. Modifying fields directly on records in Trigger.new is not allowed.
How to resolve this issue?
To achieve the desired outcome, you need to follow a different approach: query the records again, update the fields, and then execute an update DML statement. Below is a correct implementation of the trigger:
trigger SetProductCategory on OpportunityLineItem (after insert) {
    // Query the opportunity line items to fetch related Product Category from Product
    List<OpportunityLineItem> olis = [
        SELECT Id, Product_Category__c, PricebookEntry.Product2.Product_Category_new__c 
        FROM OpportunityLineItem 
        WHERE Id IN :Trigger.newMap.keySet()
    ];
    // Update the Product_Category__c field based on the Product_Category_new__c value
    for (OpportunityLineItem opplineitem : olis) {
        opplineitem.Product_Category__c = opplineitem.PricebookEntry.Product2.Product_Category_new__c;
    }
    // Perform the update operation
    update olis;
}Explanation of the Solution
- Instead of modifying Trigger.new, the records are queried again using theTrigger.newMap.keySet()to fetch the latest database state, including related fields.
- The required field (Product_Category__c) is updated on the queried records.
- A separate updateDML statement is executed to save these changes.
Why does this work?
By querying the records anew, you’re working with mutable objects, allowing modifications and subsequent updates to Salesforce records.
Alternative Solution: Using a before insert Trigger
If you’re open to using a before insert trigger, you can avoid the “Record is read-only” issue altogether. In a before trigger, changes made to Trigger.new are automatically saved without needing an additional update statement:
trigger SetProductCategory on OpportunityLineItem (before insert) {
    for (OpportunityLineItem opplineitem : Trigger.new) {
        opplineitem.Product_Category__c = opplineitem.PricebookEntry.Product2.Product_Category_new__c;
    }
}Why use after insert vs. before insert?
- Use after insertif the related fields you’re accessing (likePricebookEntry.Product2.Product_Category_new__c) depend on the successful creation of the record or are not available in thebeforecontext.
- Use before insertwhen you can set the fields before the record is saved, avoiding an additional DML operation.
By selecting the appropriate approach based on your needs, you can ensure a robust and error-free implementation.
Summing Up
In essence, tackling the “Record is read-only” error boils down to understanding the difference between before and after triggers. If you’re working in an after insert context, the key is to query the records again, make your updates, and perform a separate DML operation. Alternatively, a before insert trigger can simplify the process by allowing direct modifications to Trigger.new. The choice between these approaches depends on when your related fields are accessible, but either way, you ensure clean, efficient, and error-free code that aligns perfectly with Salesforce’s trigger execution framework.


