Error Handling for Salesforce API Integrations

An integration is only as good as what it does when a call fails. Here is how to classify Salesforce API errors, retry safely and make failures visible.

Type
Article
Topic
CRM & Engineering
Reading time
5 min
Updated
Author
GeekMidst Team
Reviewed by
Site owner
Banner for the GeekMidst article “Error Handling for Salesforce API Integrations”, covering Read the error, not just the status code and…

Most Salesforce integrations are built for the happy path and tested with ten records. Then a validation rule changes, a token expires overnight or a bulk update locks a parent account, and records quietly stop arriving. Nobody notices until a sales manager asks why last week's orders are missing.

Good error handling is not a try/catch around the HTTP call. It is a set of decisions: which failures are worth retrying, how to retry without creating duplicates, what to do with records that will never succeed, and who gets told.

Diagram showing an external ID upsert whose errorCode is classified as transient, data, authentication or configuration, leading to retry with backoff, a dead-letter store, token refresh or stopping to fix
Branch on errorCode, not the HTTP status: retry only transient failures with backoff and a ceiling, and park everything else where a named owner will see it. View full size

Read the error, not just the status code

Salesforce REST API errors return a JSON body with an errorCode, a message and, for field errors, a list of fields. The HTTP status alone is not enough to decide what to do: several quite different problems return 400, and exceeding the org's API request allocation returns an error with the code REQUEST_LIMIT_EXCEEDED. Always parse the body and branch on errorCode.

json
[
  {
    "message": "Required fields are missing: [LastName]",
    "errorCode": "REQUIRED_FIELD_MISSING",
    "fields": ["LastName"]
  }
]
Shape of a typical REST API error response. Log the whole body, not only the message.

Classifying common failures

ExamplesRetry?Action
TransientUNABLE_TO_LOCK_ROW, timeouts, 5xx responsesYes, with backoffRetry a few times, then park
CapacityREQUEST_LIMIT_EXCEEDED, HTTP 429 from middlewareLaterSlow down or pause, alert the owner
AuthenticationINVALID_SESSION_ID, HTTP 401OnceRefresh the token, retry once, then alert
DataREQUIRED_FIELD_MISSING, FIELD_CUSTOM_VALIDATION_EXCEPTION, DUPLICATE_VALUENoPark the record with the error for a human to fix
ConfigurationINVALID_FIELD, NOT_FOUND, INSUFFICIENT_ACCESS_OR_READONLYNoStop the flow and fix the integration or permissions

Make retries safe with idempotency

Retrying is only safe if doing the same thing twice has the same effect as doing it once. A POST that creates a record is not idempotent: if the first call actually succeeded but the response was lost, the retry creates a duplicate.

The standard Salesforce answer is an external ID field and an upsert. Mark a field such as External_Order_Id__c as an External ID and Unique, then upsert by that key (in REST, a PATCH to the sObject resource addressed by the external ID field and value). A retry then updates the same record instead of inserting another. If your source system has no stable key, create one before the record leaves it.

Retry with backoff and a ceiling

When you do retry, wait longer each time and add some randomness so many failed calls do not all retry at the same instant. Put a ceiling on attempts. After the ceiling, the record goes to a dead-letter store with the full error, not into a log nobody reads.

Before you start

The example below assumes a Node.js 18 or later service (for the built-in fetch) that already holds a valid access token, and an upsert endpoint keyed on an external ID. It is a sketch of the logic, not production code: token refresh, structured logging and configuration are left out.

js
const RETRYABLE_CODES = new Set(['UNABLE_TO_LOCK_ROW']);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function upsertWithRetry(url, token, payload, maxAttempts = 4) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    let res;
    try {
      res = await fetch(url, {
        method: 'PATCH',
        headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
      });
    } catch (networkError) {
      if (attempt === maxAttempts) throw networkError;
      await sleep(backoff(attempt));
      continue;
    }
    if (res.ok) return res.status === 204 ? null : res.json();

    const errors = await res.json().catch(() => []);
    const code = Array.isArray(errors) && errors.length ? errors[0].errorCode : 'UNKNOWN';
    const retryable = res.status >= 500 ? true : RETRYABLE_CODES.has(code);

    if (!retryable) {
      throw Object.assign(new Error(`Permanent failure: ${code}`), { errors, status: res.status });
    }
    if (attempt === maxAttempts) {
      throw Object.assign(new Error(`Gave up after ${attempt} attempts: ${code}`), { errors });
    }
    await sleep(backoff(attempt));
  }
}

function backoff(attempt) {
  const base = 500 * 2 ** (attempt - 1);
  return base + Math.floor(Math.random() * base);
}
Illustrative retry wrapper for an external ID upsert. Callers catch the error and write the record to a dead-letter store.

Partial success is the normal case

Bulk and collection operations rarely fail completely. The sObject Collections resource and the Composite API let you choose between all-or-none behaviour and per-record results; Bulk API jobs report successful and failed rows separately. Inside Salesforce, Apex gives you the same choice through Database methods with allOrNone set to false. Whatever you choose, handle each failed row individually and keep the successful ones.

Here is the Apex version, writing failures to a custom Integration_Error__c object (an assumption; use whatever logging object your org already has).

apex
List<Integration_Error__c> failures = new List<Integration_Error__c>();
List<Database.UpsertResult> results =
    Database.upsert(accounts, Account.External_Id__c, false);

for (Integer i = 0; i < results.size(); i++) {
    Database.UpsertResult r = results[i];
    if (r.isSuccess()) continue;
    for (Database.Error err : r.getErrors()) {
        failures.add(new Integration_Error__c(
            Record_Key__c  = accounts[i].External_Id__c,
            Status_Code__c = String.valueOf(err.getStatusCode()),
            Message__c     = err.getMessage().left(255),
            Fields__c      = String.join(err.getFields(), ',')
        ));
    }
}
if (!failures.isEmpty()) {
    insert failures;
}
Partial-success upsert in Apex. The results list is in the same order as the input list.

Make failures visible to a person

A dead-letter store is only useful if someone looks at it. Give each integration a named owner, a simple report or dashboard of parked records grouped by error code, and an alert when the count crosses a threshold or when no successful call has happened for longer than expected. Include a correlation ID (an ID you generate per message and pass through every system) so one record can be traced end to end.

Decide in advance how parked records are replayed: fixed in the source and resent, or corrected and retried from the store. Replays should go through the same idempotent upsert as normal traffic.

Common mistakes

  • Retrying validation errors. They will fail every time and consume API requests. Classify first.
  • Catching and logging to debug logs only. Debug logs expire and are not monitored.
  • Swallowing partial failures because the overall call returned 200 or the job completed.
  • Retrying a create without an external ID, which produces duplicates under load.
  • Hard-coded credentials in the integration. Use Named Credentials inside Salesforce and a secrets store outside it.

Quick checklist

  1. Every integration parses errorCode and classifies the failure.
  2. Writes are upserts on an external ID wherever the data allows.
  3. Retries use backoff with a maximum attempt count.
  4. Failed records land in a store with the payload, error and correlation ID.
  5. Someone owns the store and gets alerted.

Our integration error handling checklist expands each point into testable checks.

Is an integration losing records?

We can review how your Salesforce integrations classify, retry and report failures and help you put monitoring in place that people will actually use.

Request a technical assessment →

Tell us what you are trying to fix.

Share your Salesforce setup, goals and constraints. A practitioner reads every message and replies with questions, first thoughts or an honest view of whether you need outside help at all.

Talk to a Salesforce Expert
Book a Consultation