Salesforce Dictionary - Free Salesforce GlossarySalesforce Dictionary
All errors
Security

ENTITY_FAILED_IFLASTMODIFIED_ON_UPDATE_CHECK: record has been modified since you retrieved it

You sent an update with an `If-Unmodified-Since` header (or the API equivalent), and the record was modified by someone else after your read. The platform protected against the lost-update — refreshing your read and reapplying the change is the right behaviour.

Also seen asENTITY_FAILED_IFLASTMODIFIED_ON_UPDATE_CHECK·record has been modified since you retrieved it·If-Unmodified-Since·optimistic locking salesforce

A field service dispatcher uses a custom Lightning Web Component to assign work orders. The component lets her drag jobs from one technician to another. On a busy Tuesday, two assignments in a row succeed, and the third returns a red toast reading ENTITY_FAILED_IFLASTMODIFIED_ON_UPDATE_CHECK: record has been modified since you retrieved it. She refreshes, retries, and the same job assignment now goes through. The user wonders what just happened. The developer needs to handle this class of conflict without losing edits.

What the platform is checking

Salesforce's REST API supports optimistic concurrency through the If-Unmodified-Since HTTP header. The header carries a timestamp that the API compares to the record's current SystemModstamp. If the record was modified after the supplied timestamp, the API refuses the update and returns ENTITY_FAILED_IFLASTMODIFIED_ON_UPDATE_CHECK. The error protects against the classic lost-update problem.

The pattern works like this. The client retrieves a record at time T1. The retrieval response includes the record's modification timestamp. The user spends some time editing. At time T2, the client sends the update with an If-Unmodified-Since: T1 header. The API checks the current SystemModstamp against T1. If they match (no one modified the record between T1 and T2), the update proceeds. If they differ (someone else modified the record), the API rejects the update with the error message.

The check exists because two users editing the same record without coordination produces silent overwrites. Without the check, whoever saves last wins, and the first user's edits vanish into the database without warning. The check forces the application to surface the conflict to the user, who can decide how to merge.

The behavior depends on the client opting into the check by sending the header. Apex DML and the default Lightning record-page save flow do not use the header; they perform last-write-wins by design. The error fires only when an integration explicitly sends If-Unmodified-Since.

The broken example

A custom REST client built on Node.js that posts updates to Salesforce:

async function updateWorkOrder(workOrderId, fields) {
  const original = await sfClient.get(`/sobjects/WorkOrder/${workOrderId}`);
  const lastModified = original.headers['last-modified'];

  // User edits, time passes...

  return sfClient.patch(
    `/sobjects/WorkOrder/${workOrderId}`,
    fields,
    { headers: { 'If-Unmodified-Since': lastModified } }
  );
}

The function retrieves the record, then later issues a PATCH with If-Unmodified-Since set to the timestamp from retrieval. If another user updated the record between the GET and the PATCH, the PATCH returns 412 Precondition Failed with ENTITY_FAILED_IFLASTMODIFIED_ON_UPDATE_CHECK.

The code has no handling for the error. The thrown exception propagates to the UI, the user sees the toast, and any in-progress edits are stuck in component state with no clear recovery path.

A second shape: a Lightning Web Component that uses lightning/uiRecordApi with an explicit If-Unmodified-Since header through updateRecord calls. The conflict surfaces the same way.

A third shape: a Mulesoft or third-party integration platform configured to use optimistic concurrency by default, with no flow logic for the 412 response. The integration retries on transient failures, but a 412 is treated as a permanent failure and the record falls into an error queue.

Why the conflict happens in real usage

The default SystemModstamp is recorded at second-level precision. Two users saving the same record within the same second can both succeed, leaving the record's modstamp at the time of the later save. The client that retrieved first now has a stale timestamp.

More commonly, the conflict comes from background processes. A workflow rule, a flow, an Apex trigger, or a scheduled job updates the record while the user is editing. The user's If-Unmodified-Since timestamp is from before the background update, and the user's save fails.

Field Service in particular has a high background-update rate because the platform's own scheduling engine adjusts work orders as resources become available. Manual edits made during scheduling runs are prone to this collision.

The fix, three paths

Refresh and merge, surfacing the conflict to the user. The best approach for user-facing edits. When the conflict fires, re-fetch the current record state, compare to what the user edited, and ask the user to confirm. The user sees the other party's changes and decides whether to keep theirs.

async function updateWorkOrder(workOrderId, fields, lastModified, onConflict) {
  try {
    return await sfClient.patch(
      `/sobjects/WorkOrder/${workOrderId}`,
      fields,
      { headers: { 'If-Unmodified-Since': lastModified } }
    );
  } catch (err) {
    if (err.response?.status === 412) {
      const current = await sfClient.get(`/sobjects/WorkOrder/${workOrderId}`);
      const resolved = await onConflict(current.data, fields);
      if (resolved) {
        return sfClient.patch(
          `/sobjects/WorkOrder/${workOrderId}`,
          resolved,
          { headers: { 'If-Unmodified-Since': current.headers['last-modified'] } }
        );
      }
    }
    throw err;
  }
}

The onConflict callback receives the current server state and the user's edits, returning the resolved record. The UI can present a side-by-side diff, ask the user to merge, and resubmit with a fresh timestamp.

Drop the optimistic-concurrency check and accept last-write-wins. For backend integrations where conflicts are rare and the data is non-critical, omitting the If-Unmodified-Since header turns the API into the default last-write-wins behavior.

return sfClient.patch(`/sobjects/WorkOrder/${workOrderId}`, fields);

The change is one-line. The trade-off is that conflicts now happen silently. Use this only when the workflow can tolerate the loss of one party's edits.

Lock the record before editing. For workflows that genuinely cannot tolerate concurrent edits, Salesforce supports record locking through Apex. A FOR UPDATE clause in SOQL acquires a lock; subsequent updates from other transactions block until the lock releases.

WorkOrder wo = [SELECT Id, Status FROM WorkOrder WHERE Id = :workOrderId FOR UPDATE];
wo.Status = 'In Progress';
update wo;

The lock is transaction-scoped. As long as your edit lives within a single Apex transaction, no other Apex transaction can modify the record. The lock does not extend to API clients or to UI saves through standard record pages, so it only helps for integration code that runs as Apex.

The fixed example

A Lightning Web Component with conflict resolution baked in:

import { LightningElement, api, wire } from 'lwc';
import { getRecord, updateRecord } from 'lightning/uiRecordApi';
import { refreshApex } from '@salesforce/apex';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

export default class WorkOrderEditor extends LightningElement {
  @api recordId;
  pendingEdits = {};
  serverState = null;

  @wire(getRecord, { recordId: '$recordId', fields: ['WorkOrder.Status', 'WorkOrder.OwnerId'] })
  wiredRecord(result) {
    this.serverState = result;
  }

  async handleSave() {
    try {
      await updateRecord({
        fields: { Id: this.recordId, ...this.pendingEdits }
      });
      this.dispatchEvent(new ShowToastEvent({
        title: 'Saved',
        variant: 'success'
      }));
    } catch (error) {
      if (this.isConflict(error)) {
        await refreshApex(this.serverState);
        this.dispatchEvent(new ShowToastEvent({
          title: 'Conflict detected',
          message: 'The record changed since you opened it. Review the latest values and retry.',
          variant: 'warning'
        }));
      } else {
        this.dispatchEvent(new ShowToastEvent({
          title: 'Save failed',
          message: error.body?.message || 'Unknown error',
          variant: 'error'
        }));
      }
    }
  }

  isConflict(error) {
    return error?.body?.output?.errors?.some(
      e => e.errorCode === 'ENTITY_FAILED_IFLASTMODIFIED_ON_UPDATE_CHECK'
    );
  }
}

The component detects the conflict, refreshes the wired record state so the UI reflects the latest values, and warns the user. The user reviews and retries with their updates layered on top of the fresh state.

Edge case: timestamps and timezones

If-Unmodified-Since is an HTTP-1.1 header that expects RFC 7231 date format in GMT. Sending the local timezone or an ISO 8601 string returns a 400 Bad Request rather than a 412. Format the timestamp using toUTCString() in JavaScript or the equivalent in your language of choice.

const header = new Date(lastModifiedFromGet).toUTCString();
// "Tue, 24 May 2026 14:30:00 GMT"

The Salesforce GET response uses RFC 7231 format in the Last-Modified header, so passing that header value directly to If-Unmodified-Since is the simplest path.

Edge case: bulk updates and partial success

The error fires per record on bulk PATCH operations using /composite/sobjects. The response payload contains one outcome per record; some records succeed and others fail with this code. The client must inspect the per-record result and handle each conflict independently.

const result = await sfClient.patch('/composite/sobjects', { records: payload });
const conflicts = result.data.filter(r => r.errors?.some(e => e.statusCode === 'ENTITY_FAILED_IFLASTMODIFIED_ON_UPDATE_CHECK'));

Retrying the whole batch is wasteful. Re-fetch only the conflicting records, re-derive the desired updates, and resubmit just those.

Edge case: SystemModstamp updates from triggers

If a before update trigger modifies fields on the record being updated, the trigger's modifications also bump SystemModstamp. A user's update plus the trigger's side effect produce a single transaction with one final modstamp. From the API client's perspective, the timestamp it gets back reflects both changes, so its next If-Unmodified-Since will be correct.

The conflict from triggers only fires when one trigger commits, and a separate transaction tries to update with a timestamp from before the first trigger ran. This is the scheduled-job-during-user-edit scenario.

Test patterns

A unit test that simulates the conflict:

test('conflict triggers refresh and warning', async () => {
  const mockSfClient = {
    patch: jest.fn()
      .mockRejectedValueOnce({
        response: { status: 412, data: { errorCode: 'ENTITY_FAILED_IFLASTMODIFIED_ON_UPDATE_CHECK' } }
      })
  };
  // ... wire up component, dispatch save
  expect(toastSpy).toHaveBeenCalledWith(
    expect.objectContaining({ variant: 'warning' })
  );
});

An end-to-end test with two simultaneous browser sessions modifying the same record validates the human-facing behavior. The test seeds the record, opens two browser tabs, saves from each in sequence, and confirms the second user sees the conflict warning.

Diagnosing in production

When the error fires:

  1. Identify which client is sending If-Unmodified-Since. The standard record save flow does not, so the source is custom code.
  2. Determine whether the conflict is real (another party modified the record) or stale (the client is using an old timestamp due to a bug).
  3. If real, ensure the client refreshes and presents the conflict to the user.
  4. If stale, fix the bug. Common causes: caching the timestamp longer than the record's edit lifecycle, comparing local time to server time, or omitting the timestamp refresh after a partial save.

Defensive habits

Decide explicitly whether each integration needs optimistic concurrency. Backend integrations that overwrite computed fields usually do not. User-facing edits that may collide with other users usually do.

Surface conflicts to users rather than hiding them. A clear warning that someone else changed the record builds trust; a silent overwrite that loses edits breaks trust.

Refresh the record state on every conflict. Resubmitting with the old payload defeats the purpose of the check.

Use FOR UPDATE locks for Apex transactions that must be atomic across reads and writes. The lock prevents concurrent transactions and produces deterministic outcomes.

Conflict-resolution UX patterns

The hardest part of handling this error is not the technical code; it is the user experience when a conflict surfaces. The user has spent time editing. The save fails. The right reaction is rarely to throw the edits away.

Three UX patterns work well for different workflows.

The side-by-side diff. Show the user's draft on the left and the current server state on the right. Highlight fields that differ. Let the user pick which version of each field to keep, then save the merged result. The pattern is appropriate for high-value records where every field has independent meaning.

The "fresh start" prompt. Tell the user that the record changed and show them the current state with their edits applied on top. The user reviews and either accepts the merged version or makes further changes. The pattern is appropriate for collaborative records where users expect occasional drift.

The auto-merge. For fields where the platform can confidently merge changes (different fields edited, or a numeric field where addition is well-defined), merge silently and inform the user. The pattern is appropriate for high-volume workflows where surfacing every conflict would be intrusive.

Pick the pattern that fits the record's purpose. A pricing record needs strict conflict surfacing. A note record can auto-merge non-overlapping changes.

Pessimistic locking versus optimistic concurrency

Salesforce defaults to last-write-wins behavior. Optimistic concurrency (If-Unmodified-Since) is a client-side opt-in. Pessimistic locking (FOR UPDATE) is an Apex feature that holds the record exclusively for the duration of a transaction.

Pessimistic locking is the strictest option but the most disruptive. While one transaction holds the lock, all other transactions that try to read or write the record block. Long-held locks produce timeout errors elsewhere in the system. Use pessimistic locking for short, atomic operations where the consistency guarantee outweighs the user-visible delays.

Optimistic concurrency is a better default for most workflows. The record is never locked; the check happens at write time. Users see conflicts but rarely see delays.

Last-write-wins is the simplest option but trades data correctness for simplicity. For most operational data, the simplicity is fine because conflicts are rare. For high-stakes data (financial transactions, regulated content), the trade-off goes the other way and explicit concurrency control is required.

Quick recovery checklist

  1. Identify the client throwing the error.
  2. Confirm the timestamp logic is correct (RFC 7231, GMT, from the GET response).
  3. Implement conflict handling: refresh, merge, retry.
  4. Test with two concurrent edits.
  5. Deploy and verify.

The error is a feature, not a bug. Handling it correctly builds a workflow where users trust the system to surface real conflicts and to protect their work.

Further reading from Salesforce

Related dictionary terms

Share this fix

Share on LinkedInShare on X

Related Security errors