Error handling and API limits

Salesforce error arrays, per-record failure policies and staying within the daily API request allowance.

How Salesforce reports errors

A failed REST call answers with a JSON array of error objects - an array, because one request can violate several rules at once. Each object carries an errorCode, a human-readable message and, for field-level problems, the fields involved:

[
  {
    "errorCode": "REQUIRED_FIELD_MISSING",
    "message": "Required fields are missing: [Name]",
    "fields": ["Name"]
  }
]

Because error responses are JSON like any other, they arrive in your service as the parsed value - a list instead of the dict a successful call returns. That makes the check straightforward:

response = conn.post('/sobjects/Campaign/', record)

# Salesforce answers with a list of error objects when the request fails
if isinstance(response, list):
    error = response[0]
    error_code = error['errorCode']
    message = error['message']
    raise Exception(f'Salesforce error {error_code}: {message}')

The codes you will actually meet:

errorCodeWhat it means
REQUIRED_FIELD_MISSINGA field the object requires was not in the request
INVALID_FIELDA field name does not exist on the object - usually a typo or a missing __c
INVALID_FIELD_FOR_INSERT_UPDATEThe field exists but cannot be written, e.g. a formula field
MALFORMED_QUERYThe SOQL text does not parse
NOT_FOUNDThe record ID does not exist or was deleted
ENTITY_IS_DELETEDThe record is in the recycle bin
INVALID_SESSION_IDThe access token expired or was revoked
REQUEST_LIMIT_EXCEEDEDThe org's daily API request allowance is used up
UNABLE_TO_LOCK_ROWAnother transaction holds the record - retrying usually succeeds

Failing one record or failing the run

A service that handles one request at a time - a REST channel creating one record - should simply raise, as above. The caller receives the error and decides what to do, which is exactly what HTTP is for.

A sync that processes hundreds of records needs a policy, because one bad record must not silently take down the other 499. The pattern is to catch per record, log with enough context to find the culprit, count, and continue:

updated = 0
failed  = 0

for record in records:
    try:
        self.process_one(conn, record)
        updated += 1
    except Exception:
        failed += 1
        record_id = record['Id']
        self.logger.warning('Could not process `%s`, e:`%s`', record_id, format_exc())

self.logger.info('Sync done, updated: %s, failed: %s', updated, failed)

The end-of-run summary is what turns a log file into an answer to "did last night's sync work?". If failures should page someone, the counters are also the right place to hook a notification.

The exception to log-and-continue is an error that will affect every record the same way - INVALID_SESSION_ID or REQUEST_LIMIT_EXCEEDED means the next 499 calls will fail exactly like the first one did, so raise immediately and let the run stop.

The daily API request allowance

Every Salesforce org has a rolling 24-hour limit on API requests - from 15,000 calls per day in smaller editions upwards, shared by all integrations connected to the org, yours and everyone else's. An integration that works flawlessly in a sandbox can exhaust a production org's allowance simply by being inefficient, and when the allowance is gone, every integration in the company stops.

The current usage is one GET away:

response = conn.get('/limits/')

api_requests = response['DailyApiRequests']
remaining = api_requests['Remaining']
max_allowed = api_requests['Max']

self.logger.info('API requests remaining: %s of %s', remaining, max_allowed)

Logging this at the end of each scheduled run gives you a trend line for free, and a service that raises an alert when Remaining drops below a threshold is a few lines more.

Spending the allowance well

The techniques from the other guides in this section are also the ones that keep request counts low, so this is mostly a checklist:

  • Traverse relationships in one SOQL query instead of fetching parents record by record - one query with Account.Name in the select list replaces a thousand per-record lookups.
  • Compare before writing - a record that did not drift costs zero write calls.
  • Filter queries on LastModifiedDate so each run reads what changed, not the whole table.
  • Let the scheduler set the pace - a sync every two hours costs a predictable number of requests per day, which you can compute in advance and compare against the allowance.

Learn more