Salesforce Dictionary - Free Salesforce GlossarySalesforce Dictionary
All errors
Apex

System.DmlException: Insert failed (or Update / Upsert / Delete failed)

A DML statement (insert/update/upsert/delete) failed. The exception message contains a "first exception on row N" line that tells you which record blew up and why — that's where you actually look.

Also seen asDmlException·Insert failed·Update failed·Upsert failed

A bulk import script that worked in QA dies in production with System.DmlException: Insert failed. First exception on row 0; first error: FIELD_CUSTOM_VALIDATION_EXCEPTION, .... The full message is forty lines long. The actual cause is in the middle, between two layers of platform metadata. Finding it requires knowing how to read DmlException output.

What the platform throws

DmlException is the umbrella exception for any DML statement that fails. The exception's getMessage() returns a multi-line string formatted as:

System.DmlException: Insert failed. First exception on row N; first error: STATUSCODE, message [field-name]

Inside, the exception carries structured per-row error data accessible via these methods:

  • getNumDml(): how many rows failed.
  • getDmlId(i): the id of the i-th failing record (for updates).
  • getDmlType(i): the StatusCode enum value (REQUIRED_FIELD_MISSING, FIELD_CUSTOM_VALIDATION_EXCEPTION, INVALID_CROSS_REFERENCE_KEY, etc.).
  • getDmlFields(i): an array of field tokens implicated in the failure.
  • getDmlMessage(i): the user-facing error text for that row.

The "first exception on row 0" prefix is convenience output; the real diagnostic value is in the per-row data.

The broken example

A common pattern: an import script that catches the exception but only logs its top-level message:

public class BulkImporter {
    public static void importLeads(List<Lead> leads) {
        try {
            insert leads;
        } catch (DmlException ex) {
            System.debug(LoggingLevel.ERROR, ex.getMessage());
            throw ex;
        }
    }
}

The log shows "Insert failed" but loses the per-row detail. The user sees a generic error; the developer has to re-run with logging adjusted to see what went wrong on each row.

The fix: read every per-row error

A proper exception handler iterates the per-row data and produces an actionable diagnostic:

public class BulkImporter {
    public class ImportFailure {
        public Integer rowIndex;
        public String recordId;
        public String statusCode;
        public String message;
        public List<String> fields;
    }

    public static List<ImportFailure> importLeads(List<Lead> leads) {
        List<ImportFailure> failures = new List<ImportFailure>();
        Database.SaveResult[] results = Database.insert(leads, false);
        for (Integer i = 0; i < results.size(); i++) {
            if (!results[i].isSuccess()) {
                for (Database.Error err : results[i].getErrors()) {
                    ImportFailure f = new ImportFailure();
                    f.rowIndex = i;
                    f.recordId = leads[i].Id;
                    f.statusCode = String.valueOf(err.getStatusCode());
                    f.message = err.getMessage();
                    f.fields = new List<String>();
                    if (err.getFields() != null) {
                        for (String fld : err.getFields()) f.fields.add(fld);
                    }
                    failures.add(f);
                }
            }
        }
        return failures;
    }
}

Database.insert(records, false) is the partial-success variant. Each row gets its own SaveResult indicating success or failure. The getErrors() method on each result returns the per-row error details.

The pattern is the same for Database.update(records, false), Database.upsert(records, externalIdField, false), and Database.delete(records, false).

The fixed example, with structured response

A complete import service that returns row-by-row results:

public class BulkImporter {
    public class Result {
        @AuraEnabled public Integer rowIndex;
        @AuraEnabled public Boolean success;
        @AuraEnabled public Id recordId;
        @AuraEnabled public String error;
    }

    public static List<Result> importLeads(List<Lead> leads) {
        List<Result> results = new List<Result>();
        if (leads.isEmpty()) return results;

        Database.SaveResult[] saveResults = Database.insert(leads, false);
        for (Integer i = 0; i < saveResults.size(); i++) {
            Result r = new Result();
            r.rowIndex = i;
            Database.SaveResult sr = saveResults[i];
            if (sr.isSuccess()) {
                r.success = true;
                r.recordId = sr.getId();
            } else {
                r.success = false;
                r.error = formatError(sr);
            }
            results.add(r);
        }
        return results;
    }

    private static String formatError(Database.SaveResult sr) {
        List<String> parts = new List<String>();
        for (Database.Error err : sr.getErrors()) {
            String msg = err.getStatusCode() + ': ' + err.getMessage();
            if (err.getFields() != null && !err.getFields().isEmpty()) {
                msg += ' (fields: ' + String.join(err.getFields(), ', ') + ')';
            }
            parts.add(msg);
        }
        return String.join(parts, '; ');
    }
}

The caller gets a list of typed results. Each result names the row, whether it succeeded, the new id (on success), and a formatted error (on failure). Building a UI on top is trivial: walk the list, show success/failure status per row.

The StatusCode enum values

DmlException.getDmlType(i) returns one of about 200 StatusCode values. Knowing the common ones speeds diagnosis:

  • REQUIRED_FIELD_MISSING: a required field was null. See required-field-missing.
  • FIELD_CUSTOM_VALIDATION_EXCEPTION: a validation rule blocked the save.
  • INVALID_CROSS_REFERENCE_KEY: a lookup field's id doesn't exist or is the wrong type.
  • DUPLICATE_VALUE: a unique-field constraint was violated.
  • INSUFFICIENT_ACCESS_OR_READONLY: the user lacks edit permission.
  • STRING_TOO_LONG: a text value exceeds the field length.
  • INVALID_FIELD_FOR_INSERT_UPDATE: tried to set a read-only field.
  • ENTITY_IS_LOCKED: the record is in approval.
  • ENTITY_IS_DELETED: the record is in the recycle bin.
  • STORAGE_LIMIT_EXCEEDED: the org is out of storage.

Each value has its own remediation. For bulk imports, the most efficient triage is to group failures by status code and handle each group.

All-or-nothing vs partial-success

Two variants of every DML operation:

VariantBehavior on first failure
insert records; or update records;All-or-nothing. One row fails, the whole batch rolls back.
Database.insert(records, false) etc.Partial success. Failed rows stay failed; successful rows commit.

For user-facing operations (a UI form), all-or-nothing is usually right: you don't want half a save. For bulk imports, partial-success is usually right: one bad row shouldn't poison thousands of good rows.

The third variant, Database.SaveOptions, gives finer control (e.g., allowing partial success with specific error-handling rules). Most code uses the boolean variant.

Catching vs reading SaveResult

A common confusion: do you catch DmlException or do you read SaveResult?

  • All-or-nothing DML throws DmlException on any failure. Catch it.
  • Partial-success DML returns SaveResult[]. Don't catch; iterate the array.

If you use partial-success and also wrap in try/catch, the catch never fires (the partial-success path doesn't throw). Reviewers sometimes write the try/catch out of habit; the result is "successful" failures that go unnoticed.

A subtle case: trigger errors masquerading as DML errors

A custom Apex trigger that calls record.addError('message') produces output identical to a FIELD_CUSTOM_VALIDATION_EXCEPTION in the DML result. The user can't tell whether the rule came from a Validation Rule (declarative) or a trigger (Apex). The diagnostic distinction:

  • Validation rules: visible in Setup → Object Manager → object → Validation Rules.
  • Apex addError: visible only in source code; greppable but invisible from Setup.

For a complete picture of why a save can fail on an object, audit both layers.

When the row index doesn't match your input

getNumDml() returns the failure count. getDmlId(i) and getDmlType(i) walk through them in order. The order matches the order of the input list, but the indices are the failing-row indices within the input, not necessarily sequential.

For a 100-row input where rows 0, 5, and 17 fail, the exception's getNumDml is 3 and the getDmlIndex calls return 0, 5, 17 respectively. The other rows succeeded.

A real-world debugging walkthrough

A team's nightly job started failing with DmlException and no clear cause. The naive log handler only showed "Insert failed; First exception on row 0." The team upgraded the handler to walk every error:

catch (DmlException ex) {
    for (Integer i = 0; i < ex.getNumDml(); i++) {
        System.debug(LoggingLevel.ERROR,
            'Row ' + ex.getDmlIndex(i)
            + '; Id: ' + ex.getDmlId(i)
            + '; Code: ' + ex.getDmlType(i)
            + '; Fields: ' + ex.getDmlFields(i)
            + '; Message: ' + ex.getDmlMessage(i));
    }
}

The next nightly run produced a log showing 12 distinct failure rows, all with INVALID_CROSS_REFERENCE_KEY on the Account__c field. The cause: an upstream system had renamed account ids without notifying the import. Five minutes of source-system investigation; thirty minutes to fix the upstream's notification logic.

The diagnostic that took five minutes would have taken hours without per-row error data. Upgrading the catch handler is one of the highest-ROI changes a team can make.

A practical pattern: lossless error logging

For audit and post-hoc analysis, log every DML failure to a custom DML_Error_Log__c object:

List<DML_Error_Log__c> logRows = new List<DML_Error_Log__c>();
for (Integer i = 0; i < saveResults.size(); i++) {
    if (!saveResults[i].isSuccess()) {
        for (Database.Error err : saveResults[i].getErrors()) {
            logRows.add(new DML_Error_Log__c(
                Object_Type__c = 'Lead',
                Row_Index__c = i,
                Status_Code__c = String.valueOf(err.getStatusCode()),
                Message__c = err.getMessage().left(2000),
                Field_List__c = String.join(err.getFields() ?? new String[]{}, ',')
            ));
        }
    }
}
if (!logRows.isEmpty()) insert logRows;

A weekly report over the log shows trends: which status codes fire most often, which fields are most problematic, whether new patterns emerge after a release. The diagnostic capacity scales beyond what individual incident triage can provide.

When the exception is wrapped

Some service layers catch DmlException and rethrow as a custom exception with a less-informative message:

catch (DmlException ex) {
    throw new ImportException('Import failed; check the logs.');
}

The custom exception is fine if the original DmlException is preserved (chained), or if the per-row details are extracted before the throw. Without preservation, the calling code loses the diagnostic value.

The cleanest pattern: preserve the per-row details in the new exception:

public class ImportException extends Exception {
    public List<BulkImporter.Result> failures;
}

catch (DmlException ex) {
    ImportException ie = new ImportException('Import failed', ex);
    ie.failures = extractFailures(ex);
    throw ie;
}

The custom exception carries structured data and the original exception. The calling code can inspect both.

A subtle bug: silent partial failures

A common pattern that goes wrong:

Database.SaveResult[] results = Database.insert(records, false);
// (no inspection of results)

The DML completed; some rows succeeded; some failed. The code moves on. No exception, no log, no alarm. Downstream code that expected all rows to be inserted operates on incomplete data.

Always inspect SaveResult. Either log every failure or assert that every row succeeded. Silent partial failures are the worst kind of bug because they don't surface until much later, often as data quality issues without obvious cause.

Per-row error vs trigger error

A DML batch fails for two distinct reasons:

  • Per-row validation: each record is evaluated independently. Validation rules, required fields, lookup integrity. Failures are per-row.
  • Trigger-side error: an Apex trigger on the object throws (via uncaught exception or addError on the whole context). Often blocks the entire batch even with partial-success enabled.

For trigger-side errors, the exception's per-row data may name the first triggering record. The fix lives in the trigger code, not in the data. Diagnose by checking whether the same data succeeds when triggers are temporarily disabled (in a sandbox; never disable triggers in production for diagnostic purposes).

Field-level error vs record-level error

Database.Error.getFields() returns the fields the error implicates. Some errors are field-specific (REQUIRED_FIELD_MISSING, STRING_TOO_LONG); others are record-level (ENTITY_IS_LOCKED, INSUFFICIENT_ACCESS_OR_READONLY).

For field-specific errors, surface the field name in the UI inline at the field. For record-level errors, show a banner. The distinction makes user feedback more actionable.

The convertLead variant

Database.convertLead and Database.LeadConvert follow the same structured-result pattern. Convert results are LeadConvertResult[], each with isSuccess(), getErrors(), and ids for the created Account/Contact/Opportunity. Treat them the same way you treat insert/update results.

Async DML in Batch and Queueable

Inside Batch Apex execute() or a Queueable's execute, DML follows the same patterns. The structured SaveResult is the right diagnostic for async DML too. Async DML doesn't auto-retry on failure; if you want retry, build it explicitly.

A short reference for the most useful methods

When you're staring at a DmlException and trying to extract diagnostic value, these methods do the heavy lifting:

MethodReturnsUse for
getNumDml()IntegerCount of failed rows
getDmlIndex(i)IntegerPosition in the original input list
getDmlId(i)IdRecord id (null on insert)
getDmlType(i)StatusCodeProgrammatic switch on failure category
getDmlMessage(i)StringUser-readable description
getDmlFields(i)List<String>Field tokens implicated
getDmlFieldNames(i)List<String>Field labels (display-name versions)

Pair the methods with structured logging and your incident response gets faster.

A note on the user-facing error message

getDmlMessage(i) is usually the right thing to show to a user. It's the formatted message that includes the rule's wording (for validation rules), the field's label (for required-field errors), or the missing dependency (for cross-reference errors).

For non-Apex callers (REST API, SOAP API), the per-row error structure is part of the response payload. The same diagnostic value exists; the consumer just walks the JSON or XML instead of the Apex SaveResult array.

Further reading from Salesforce

Related dictionary terms

Share this fix

Share on LinkedInShare on X

Related Apex errors