Find caused by: in the message. Everything after it is your actual bug, and the Trigger.X: line N fragment at the end is its location. Fix that. Catching or retrying the wrapper accomplishes nothing — the nested exception will throw again on the next attempt.
What the platform is telling you
When you call insert or update, Salesforce runs a save pipeline: before-triggers, system validation, custom validation rules, the save itself, after-triggers, assignment and workflow rules, record-triggered flows, roll-up summary recalculation. Anything in that pipeline can abort the operation.
When the abort comes from your automation rather than from a platform rule, Salesforce needs a status code for it. It does not have one per possible cause, so it uses CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY for all of them and appends the real exception to the message text. The status code is a category — "the entity could not be written because attached logic said no" — not a diagnosis.
Two consequences follow. First, e.getDmlStatusCode(0) in a catch block is useless for branching on this: every distinct underlying bug reports the same code. Second, the whole DML statement fails, not just the offending row, unless the caller opted into partial success.
Reading the message
System.DmlException: Insert failed. First exception on row 0;
first error: CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY,
AccountTrigger: execution of BeforeInsert
caused by: System.NullPointerException: Attempt to de-reference a null object
Trigger.AccountTrigger: line 24, column 1: []- "row 0" — the index in the list you passed to DML. Maps straight back to your source record.
- The name before the colon (
AccountTrigger) — the trigger that threw. If it names a flow interview instead, your automation is declarative, not Apex. - "execution of BeforeInsert" — which trigger event. Before vs after narrows the handler method immediately, and tells you whether the record had an Id yet.
- Everything after
caused by:— the actual exception and message. This is the bug. - The trailing
Trigger.X: line N, column M— the throw site inside the trigger file, not inside the handler class. If your trigger is one line delegating to a handler, this points at that line and you need the nested stack from the debug log or a local run. - The trailing
[]— the list of fields the error was attached to. Empty means it was a record-level error rather than anaddErroron a specific field.
The causes, ranked
1. An unhandled exception in a trigger
The dominant case. Whatever the nested exception is, fix it as you would anywhere else — the wrapper is incidental. The three that show up most in trigger bodies:
- NullPointerException — a lookup traversal on a record where the lookup is empty, or
Trigger.oldMapdereferenced during an insert, where it is null. See the NPE page. - QueryException: List has no rows for assignment to SObject — a single-row SOQL assignment inside a trigger that assumed the parent exists.
- A nested DmlException — the trigger writes related records and that write fails validation. Your message then contains two status codes, and the inner one is the real one.
trigger OpportunityTrigger on Opportunity (before insert) {
for (Opportunity o : Trigger.new) {
// Throws when the map missed, or when Trigger.oldMap is null on insert
Account a = [SELECT Id, OwnerId FROM Account WHERE Id = :o.AccountId];
o.OwnerId = a.OwnerId;
}
}
// Bulk-safe and null-safe: one query, guarded lookup
trigger OpportunityTrigger on Opportunity (before insert) {
Set<Id> accountIds = new Set<Id>();
for (Opportunity o : Trigger.new) {
if (o.AccountId != null) accountIds.add(o.AccountId);
}
Map<Id, Account> accounts = new Map<Id, Account>(
[SELECT Id, OwnerId FROM Account WHERE Id IN :accountIds]
);
for (Opportunity o : Trigger.new) {
Account a = accounts.get(o.AccountId);
if (a != null) o.OwnerId = a.OwnerId;
}
}2. A record-triggered flow that failed
Identical wrapper, different nested content — the message names a flow and an element rather than a trigger and a line. The flow's own fault email and the Flow error log carry the element-level detail the DML message truncates. Common inner causes: a Get Records element that returned nothing followed by an unguarded field reference, and a flow-invoked Apex action that threw.
3. addError() used deliberately
When a trigger calls addError() as a business rule, the caller sees this same status code with your own message as the text. Nothing is broken — the automation is working. The fix is at the caller: either satisfy the rule, or switch to partial-success DML so the valid rows still commit:
Database.SaveResult[] results = Database.insert(contacts, false);
for (Integer i = 0; i < results.size(); i++) {
if (!results[i].isSuccess()) {
for (Database.Error e : results[i].getErrors()) {
// e.getMessage() carries the addError text — the useful part.
// e.getStatusCode() is CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY for all of them.
System.debug(contacts[i].LastName + ': ' + e.getMessage());
}
}
}4. A governor limit hit inside the save pipeline
A recursive trigger, or a trigger that queries inside a loop over a 200-record chunk, exhausts SOQL or DML limits and the nested exception is a System.LimitException. The wrapper makes this look like a trigger logic bug when it is a bulkification bug — the tell is that it only appears above a certain record count. See Too many SOQL queries: 101.
5. The trigger writes records the running user cannot touch
Nested INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY or INSUFFICIENT_ACCESS_OR_READONLY. Reproduces only under the restricted user, which is why it survives to production — the developer tested as an admin. Test the path under System.runAs with a realistically-permissioned user, and mark the handler class without sharing only if that is a deliberate decision.
6. The org is in read-only mode
The one variant with no nested exception: CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY, The organization is in read-only mode. A maintenance window or a sandbox refresh is in progress. Nothing to fix in code; retry after the window, and make sure integrations treat it as retryable rather than as data corruption.
Don't catch the wrapper
A catch (DmlException e) around the DML that logs and swallows is the single most common wrong response to this error. It converts a loud failure into a missing record nobody notices for a month. If you catch it, either re-throw with context, or use partial-success DML and handle each SaveResult explicitly. And never catch it inside the trigger to keep the save going — the platform has already decided the row is rejected.
Getting the nested cause without parsing a string
The honest note first: Nimbus does not emit the literal CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY, X: execution of BeforeInsert caused by: text for trigger failures — it reserves that status code for the read-only-mode case. What it does instead is give you the thing the platform buries in that string.
When a trigger body throws, Nimbus re-raises it at the DML call site as a System.DmlException with the original message preserved, the original exception kept as getCause(), and the trigger's own frame kept on the stack trace — so existing catch (DmlException e) code behaves as it does in the org, and the nested cause is an object you can inspect rather than a substring you have to parse.
On top of that, nimbus explain reports the exception type, the source file and line, and the SOQL and DML executed immediately before the failure — which is the trigger-side context the org's message gives you only as a line number.
# Reproduce the failing DML locally, triggers and flows firing as they do in the org
nimbus test "OpportunityTriggerTest.testOwnerDefaulting"
# The full account: exception, cause, file and line, preceding SOQL/DML
nimbus explain OpportunityTriggerTest.testOwnerDefaulting
# Machine-readable, same payload
nimbus explain OpportunityTriggerTest.testOwnerDefaulting --json