Salesforce Dictionary - Free Salesforce GlossarySalesforce Dictionary
All errors
Apex

System.SObjectException: SObject row was retrieved via SOQL without querying the requested field

You read a field on a queried SObject that wasn't included in the SELECT clause. Apex stores SObjects sparsely — a field absent from SELECT throws this exception when accessed, even if it has a value in the database.

Also seen asSObject row was retrieved via SOQL without querying the requested field·SObjectException: SObject row was retrieved·without querying the requested field

A trigger that runs on every Account update has been working for three years. A junior developer adds a new feature that reads a flag on the Account before sending a notification. The deployment passes tests. Two hours after the release, the team receives System.SObjectException: SObject row was retrieved via SOQL without querying the requested field: Account.Notification_Enabled__c. The exception fires in production but never appeared in the developer's sandbox testing. The team needs to understand why.

What the platform is checking

The Apex runtime enforces a contract between SOQL queries and SObject field access. When you query a record, the resulting SObject contains only the fields named in the SELECT clause. Reading any other field on that SObject throws SObjectException: SObject row was retrieved via SOQL without querying the requested field.

The contract exists for two reasons. The first is performance. If field access on an SObject silently triggered an extra query for missing fields, every line of code could fire an unexpected SOQL statement and developers would lose visibility into governor limits. The second is correctness. The platform refuses to return stale or uncovered data; if you did not ask for the field, the platform will not pretend it has the value.

The exception is thrown at the point of access, not at the point of query. The query succeeds because it returned the fields you asked for. The exception fires later when code reads a field the query did not include.

Field reads through related objects follow the same rule. account.Owner.Email requires the SELECT clause to include Owner.Email. Reading account.Owner returns the related User SObject, but the User SObject contains only the related fields you projected. Accessing account.Owner.Email without including it in SELECT throws.

The broken example

A trigger handler that computes a notification list:

public class AccountTriggerHandler {
    public static void afterUpdate(List<Account> newRecords, Map<Id, Account> oldMap) {
        List<Id> accountIds = new List<Id>();
        for (Account a : newRecords) {
            accountIds.add(a.Id);
        }
        Map<Id, Account> fullAccounts = new Map<Id, Account>([
            SELECT Id, Name, Industry FROM Account WHERE Id IN :accountIds
        ]);
        for (Account a : newRecords) {
            Account full = fullAccounts.get(a.Id);
            if (full.Notification_Enabled__c) {  // Throws: field not queried
                NotificationService.send(full.Id);
            }
        }
    }
}

The query selects Id, Name, Industry. The handler reads full.Notification_Enabled__c. The platform throws because the field is not in the SELECT.

The bug evaded sandbox testing because the test setup used new Account(Notification_Enabled__c = true, ...) and then passed the in-memory Account directly to the handler. The in-memory record carried all the fields the test set. Only when the handler queries from the database does the field-projection contract apply, and the production trigger context queried through Trigger.new (which provides the record without all fields) and then re-queried via SOQL.

A second shape: accessing a related field that was not included in the SELECT.

List<Opportunity> opps = [SELECT Id, Name, AccountId FROM Opportunity];
for (Opportunity o : opps) {
    System.debug(o.Account.Name);  // Throws: Account.Name not queried
}

The SELECT projects AccountId, which is the lookup, not the parent fields. o.Account would itself be null because the parent was not asked for, and o.Account.Name throws.

A third shape: Trigger.new in an after-insert context, accessing a field populated by another trigger or flow.

trigger ContactTrigger on Contact (after insert) {
    for (Contact c : Trigger.new) {
        String value = c.Custom_Field__c;  // Sometimes populated, sometimes not
    }
}

Trigger.new returns the record as it stands at the trigger context's start. Fields populated by before insert triggers are present; fields populated by after insert triggers from other handlers or by record-triggered flows that run later are not. The field is null, not missing, so no exception fires, but the value the developer expected is absent. The exception form of this issue appears when the developer re-queries Contact in the after-insert handler and forgets to include the custom field.

Why the exception is so easy to miss in sandbox

Many sandbox tests build SObjects in memory and pass them through code without querying. In-memory SObjects carry every field you set on them. The field-projection rule only applies to records retrieved via SOQL.

Production trigger contexts almost always involve SOQL re-queries because triggers receive lightweight Trigger.new records and frequently need richer data. The re-query is where the projection rule bites.

The mismatch between in-memory test data and queried production data is the most common root cause of "but it works in the sandbox" reports for this exception.

The fix, three paths

Add the field to the SELECT clause. The simplest fix. Every field your code reads should appear in the query that produced the record.

Map<Id, Account> fullAccounts = new Map<Id, Account>([
    SELECT Id, Name, Industry, Notification_Enabled__c
    FROM Account
    WHERE Id IN :accountIds
]);

The fix is one line. The discipline is to update both producer (query) and consumer (field access) together, every time the consumer adds a field.

Use dynamic SOQL with a centralized field list. For codebases where many components need similar Account data, defining the field list in one place reduces drift.

public class AccountQueries {
    public static final List<String> STANDARD_FIELDS = new List<String>{
        'Id', 'Name', 'Industry', 'Notification_Enabled__c', 'OwnerId'
    };
    public static Map<Id, Account> byIds(Set<Id> ids) {
        String query = 'SELECT ' + String.join(STANDARD_FIELDS, ', ') +
            ' FROM Account WHERE Id IN :ids';
        return new Map<Id, Account>((List<Account>) Database.query(query));
    }
}

Adding a field to STANDARD_FIELDS extends every caller. The trade-off is that the field set may grow beyond what any one caller needs, costing CPU and memory.

Use the Schema describe to introspect the record before access. For defensive code that wants to read a field if it was queried and fall back otherwise:

Map<String, Object> populated = account.getPopulatedFieldsAsMap();
if (populated.containsKey('Notification_Enabled__c')) {
    Boolean enabled = (Boolean) populated.get('Notification_Enabled__c');
    if (enabled) {
        // ...
    }
}

getPopulatedFieldsAsMap returns only the fields the record actually carries. Checking before access avoids the exception. The pattern is verbose and most readers prefer the direct fix of querying the field, but for libraries that work with records from many callers, the defensive pattern can be appropriate.

The fixed example

A refactored trigger handler:

public class AccountTriggerHandler {
    public static void afterUpdate(List<Account> newRecords, Map<Id, Account> oldMap) {
        Set<Id> accountIds = new Set<Id>();
        for (Account a : newRecords) {
            accountIds.add(a.Id);
        }

        Map<Id, Account> fullAccounts = new Map<Id, Account>([
            SELECT Id, Name, Industry, OwnerId, Notification_Enabled__c, Owner.Email
            FROM Account
            WHERE Id IN :accountIds
        ]);

        List<Id> toNotify = new List<Id>();
        for (Account a : newRecords) {
            Account full = fullAccounts.get(a.Id);
            if (full != null && full.Notification_Enabled__c == true) {
                toNotify.add(full.Id);
            }
        }

        if (!toNotify.isEmpty()) {
            NotificationService.send(toNotify);
        }
    }
}

The query now includes every field the handler reads, including the related Owner.Email. The handler is bulkified (one notification call with a list, not one per record). The null check defends against records that were deleted between the trigger context and the re-query.

Edge case: parent fields and the dot operator

account.Owner.Email is a two-level traversal. The SELECT must include Owner.Email, not just OwnerId. The platform treats parent-field projections as a separate path from the lookup id.

[SELECT Id, OwnerId FROM Account]                    // Cannot read account.Owner.Email
[SELECT Id, Owner.Email FROM Account]                // Can read account.Owner.Email
[SELECT Id, Owner.Email, Owner.Profile.Name FROM Account]  // Can read both

The traversal is limited to five levels in standard SOQL. Deeper traversals require separate queries.

Edge case: child-relationship subqueries

A subquery in SELECT projects the child relationship into the parent record. The fields available on the child are limited to those in the subquery.

List<Account> accounts = [
    SELECT Id, Name, (SELECT Id, Status FROM Cases) FROM Account
];
for (Account a : accounts) {
    for (Case c : a.Cases) {
        System.debug(c.Subject);  // Throws: Subject not queried in subquery
    }
}

The fix is to add Subject to the subquery's SELECT. The parent's SELECT field list and the child's SELECT field list are independent.

Edge case: dynamic SObject access

When working with SObjects of unknown type, record.get('Field_Name__c') returns null for fields not in the query rather than throwing. The dynamic accessor bypasses the projection check.

Object value = account.get('Notification_Enabled__c');  // Returns null, no exception

This is occasionally useful but can hide bugs. A field that should be queried but is not silently returns null, and downstream logic might branch incorrectly. Use the dynamic accessor only when the indirection is genuinely needed, such as in field-mapping libraries.

Edge case: formula fields and lookups

Formula fields require the same projection as regular fields. A formula that references parent fields still appears as a single column on the queried object.

[SELECT Id, Owner_Full_Name__c FROM Account]  // Owner_Full_Name__c is a formula
account.Owner_Full_Name__c                     // Works
account.Owner.Name                              // Throws: Owner.Name not queried

The formula handles the parent reference internally; the platform projects only the formula result, not the underlying parent fields.

Edge case: querying with FIELDS() macros

API version 51.0 and later support FIELDS(ALL), FIELDS(STANDARD), and FIELDS(CUSTOM) macros. The macros expand to all fields of the specified type at query time.

[SELECT FIELDS(ALL) FROM Account WHERE Id = :accountId LIMIT 200]

The query retrieves every field on Account. The projection rule is fully satisfied for every field access. The trade-off is performance and memory; pulling all fields when you need three is wasteful for high-volume queries.

FIELDS(ALL) requires a LIMIT clause to prevent unbounded queries. Use the macro when the field list is genuinely unpredictable, not as a lazy default.

Test patterns

Tests that query the way production does, then access fields the way production does:

@IsTest
static void handlerReadsNotificationField() {
    Account a = new Account(
        Name = 'Test',
        Notification_Enabled__c = true
    );
    insert a;

    Test.startTest();
    a.Industry = 'Banking';
    update a;
    Test.stopTest();

    Integer notificationsSent = [SELECT COUNT() FROM Notification__c WHERE Account__c = :a.Id];
    System.assertEquals(1, notificationsSent);
}

The test exercises the full path: insert, update (which fires the trigger), and observable side-effect query. The handler's re-query path is exercised because the trigger executes against persisted data, not against the in-memory record.

Avoid tests that build SObjects in memory and pass them directly to handlers. Such tests skip the SOQL projection rule entirely and miss bugs that fire only in production.

Diagnosing in production

When the exception fires:

  1. Read the message; it names the field that was accessed and was not in the query.
  2. Find the query that produced the record. The stack trace points to the access; the query is upstream of that line.
  3. Add the named field to the SELECT clause.
  4. Add a regression test that goes through the full insert-then-query path.
  5. Deploy and verify.

Most incidents resolve in minutes once the message is read; the message itself contains the fix.

Defensive habits

When adding a field reference to existing code, update the query in the same commit. Code reviews should flag any new record.FieldName access without a corresponding SELECT update.

Avoid passing in-memory SObjects to methods that internally re-query the same record. The shape mismatch (in-memory has all fields, queried has projected fields) is a frequent source of "works in test, fails in production" bugs.

Use FIELDS(ALL) sparingly. The convenience comes at a real cost in CPU and memory, and tests written against it cannot catch projection bugs in code that uses precise field lists.

Adopt a field-list helper for objects accessed from many places. Centralizing the field list reduces drift and makes it easy to add new fields globally.

A pattern for evolving field lists

In codebases where many components query the same object, each component tends to define its own field list. Over time, the field lists drift; a component reads a field that its own query forgot to include. The fix is to introduce a single Selector class per object that owns the canonical field list.

public class AccountSelector {
    public static final List<String> BASE_FIELDS = new List<String>{
        'Id', 'Name', 'Industry', 'OwnerId', 'CreatedDate'
    };

    public static Map<Id, Account> selectByIds(Set<Id> ids, List<String> extraFields) {
        List<String> allFields = new List<String>(BASE_FIELDS);
        if (extraFields != null) allFields.addAll(extraFields);
        String query = 'SELECT ' + String.join(allFields, ', ') +
            ' FROM Account WHERE Id IN :ids';
        return new Map<Id, Account>((List<Account>) Database.query(query));
    }
}

Callers request extra fields per use case. The Selector ensures the base fields are always present. Drift is contained because the field list is defined once.

The pattern is part of the FFLib Apex Common framework and is widely adopted in larger orgs. Adopting it incrementally is feasible; start with one object and extend as the value becomes apparent.

Polymorphic relationships and field projection

The Owner field can point at either a User or a Queue. Reading parent fields on Owner requires the SOQL to use a polymorphic projection.

[
    SELECT Id, Owner.Type,
        TYPEOF Owner
            WHEN User THEN Email, Profile.Name
            WHEN Group THEN DeveloperName
        END
    FROM Case
]

The TYPEOF clause projects different fields depending on the actual type of the related record. Without TYPEOF, accessing case.Owner.Email works only when the Owner is a User; it throws on Queue-owned records because Group has no Email field.

The projection rule applies inside each TYPEOF branch. Fields you read in code must appear in the corresponding WHEN block.

Quick recovery checklist

  1. Read the exception message to identify the field.
  2. Find the producing query in your code.
  3. Add the field to SELECT.
  4. Test that the trigger or handler runs against re-queried data, not in-memory data.
  5. Deploy.

The SObjectException is one of the cleanest errors to fix because the message names the exact field. The harder work is preventing recurrences through good code-review and testing habits.

Further reading from Salesforce

Related dictionary terms

Share this fix

Share on LinkedInShare on X

Related Apex errors