SOQL returns old value after UI update: diagnosis for developers
A field update made through the Salesforce User Interface (UI) shows up in the client view right away, and then a follow-up SOQL query in Apex returns the old, stale value. The behavior is counter-intuitive, but it usually comes from transaction scope, database caching, or the timing of the commit, especially in complex execution contexts.
The symptom: UI vs. Apex data mismatch
Consider the scenario described:
- Action: a user modifies a custom field (e.g.,
Date_Awarded__c) on anOpportunityrecord in the Salesforce UI. - UI observation: the UI displays the new value immediately, and the
LastModifiedDatefield updates correctly. - Apex observation: a SOQL query for the same record, run immediately after, returns the pre-update value for
Date_Awarded__c, whileLastModifiedDatereflects the update.
-- UI shows: Date_Awarded__c = 2026-XX-XX
SELECT Id, Date_Awarded__c, LastModifiedDate
FROM Opportunity
WHERE Id = :recordId
-- Apex SOQL returns: Date_Awarded__c = (Old Value), LastModifiedDate = (New Timestamp)
Root cause analysis: transaction context and commit timing
When data is modified directly through the UI, the change is usually committed to the database as part of the standard save operation the client-side framework initiates (the Aura or LWC rendering process). What decides whether Apex sees it is the context the Apex code runs in.
1. Implicit transaction context
If the Apex code running the SOQL query fires synchronously as part of the same UI save operation, for example an after update trigger or a method called directly from a component save handler, the record might not yet be visible to later queries within that same transaction scope.
Salesforce enforces transaction isolation. Changes made earlier in a running transaction are not guaranteed to be visible to later queries in that transaction unless the executing Apex context issued those changes itself through DML. When the UI saves the record, the running context knows the record ID and its new state, but a standalone SOQL query might still hit a potentially uncommitted or temporarily cached version, or a different snapshot of the database state.
2. Apex execution context
If the Apex runs after the UI save operation has fully completed, say it was triggered by an asynchronous event the save fired, or by a separate later user action, the data should generally be present. If it is still stale, consider:
- Custom caching mechanisms: any Apex code or external caching layer in the path might be serving stale data, though that is unlikely for an immediate UI-to-Apex call.
- Asynchronous queuing: if the UI action triggered an asynchronous process such as a Queueable or future method that runs later, the initial execution context might complete before the asynchronous operation finishes saving.
Developer solutions and workarounds
The goal is to force Apex to read the most recently committed state, getting past any transaction-level visibility issue.
1. Utilizing refresh() or re-querying in the UI layer
If this happens during the LWC or Aura lifecycle immediately after a save, make the component explicitly refresh its wire adapters or imperative calls to re-fetch the record from the server once the save completes. That is a client-side fix for a client-side perception problem, and it leaves the Apex alone.
2. Forcing a database read (Apex solution)
If your Apex logic has to run immediately after the UI save and read the committed value, the most reliable approach is usually to skip the standard SOQL query and read the value from the record variable the DML produced, or to reread the record explicitly with Database.query or SObject.refresh() where the context supports it.
In an Apex Trigger context (after update), the record passed into the trigger context variables (Trigger.newMap) already contains the committed database values, or the values as of the end of the transaction. Reading the field there is preferred over a SOQL query against the same record ID within the same transaction scope.
Reading from trigger context, the recommended approach in triggers:
// Inside an after update trigger
for (Opportunity opp : Trigger.new) {
// opp.Date_Awarded__c will reflect the value saved by the UI or previous DML
System.debug('Trigger new value: ' + opp.Date_Awarded__c);
}
Re-loading the record explicitly, where your execution context allows it, can force a fresh database pull. It does not apply universally to every standard scenario:
Opportunity o = [SELECT Id, Date_Awarded__c FROM Opportunity WHERE Id = :recordId FOR UPDATE];
// If the UI update happened outside this immediate transaction, this should now read correctly.
The safest bet: if the UI update is the source of the commit and you are reading it back in Apex that runs immediately after, such as an after insert/update trigger, rely on the data in Trigger.new or the DML result variable rather than a new SOQL query.
Leave a Comment