Skip to main content

Handling SOQL Query Exceptions in Salesforce Apex: Best Practices & Limits Guide

In plain words: Query Exceptions in Salesforce Apex occur when database queries encounter syntax errors, runtime mismatches (such as expecting a single record when zero or multiple return), or breach multitenant governor limits like query counts and row limits. Handling these exceptions gracefully and designing selective, bulkified SOQL queries prevents unexpected transaction rollbacks and improves application stability.

Querying the database using SOQL (Salesforce Object Query Language) is a fundamental part of Apex development. However, queries can fail at runtime due to malformed syntax, unhandled cardinality mismatches, or governor limit violations. Understanding these exceptions and applying defensive coding patterns ensures your application runs reliably under heavy enterprise workloads.

1. Common Query Exceptions in Salesforce Apex

Salesforce triggers several distinct exception types during query execution:

  • System.QueryException: Thrown when dynamic SOQL syntax is invalid, when referencing non-existent fields or objects, or when assigning a SOQL result directly to a single sObject variable when the query returns zero records or more than one record.
  • System.LimitException: Too many SOQL queries: 101: Occurs when a transaction exceeds the synchronous limit of 100 SOQL queries (or 200 in asynchronous context), usually caused by querying inside loops.
  • System.QueryException: Non-selective query against large table: Thrown when querying standard or custom objects with over 200,000 records without selective filters on indexed fields.
  • System.LimitException: Too many query rows: 50001: Triggered when a single transaction retrieves more than 50,000 total sObject records across all queries.
360 Query Limits & Exception Blueprint:
  • Synchronous Query Count: 100 SOQL queries per transaction.
  • Total Retrieved Rows Limit: 50,000 rows per transaction.
  • Heap Size Safeguard: 6 MB (Synchronous) / 12 MB (Asynchronous).
  • Defensive Pattern: Always assign query results to a List<sObject> rather than a single sObject variable.

2. Defensive Query Patterns and Error Handling

The most frequent QueryException occurs when developers expect exactly one record and write code like Account acc = [SELECT Id FROM Account WHERE Name = :inputName];. If no match is found, or if duplicate accounts exist, Salesforce immediately throws a fatal exception.

Best Practice Pattern: Defensive List Assignment & User Mode Querying
public with sharing class AccountQueryService {

    /**
     * @description Safely queries an Account by name without throwing runtime QueryExceptions
     */
    public static Account getAccountSafely(String accountName) {
        if (String.isBlank(accountName)) {
            return null;
        }

        // 1. Always assign to a List to handle 0, 1, or multiple records safely
        List<Account> accounts = [
            SELECT Id, Name, Industry, AnnualRevenue 
            FROM Account 
            WHERE Name = :accountName.trim() 
            WITH USER_MODE 
            LIMIT 1
        ];

        // 2. Safely evaluate list size rather than risking a single-record QueryException
        return !accounts.isEmpty() ? accounts[0] : null;
    }

    /**
     * @description Dynamic query execution wrapped in structured try-catch blocks
     */
    public static List<sObject> executeDynamicQuery(String queryString) {
        try {
            return Database.query(queryString, AccessLevel.USER_MODE);
        } catch (QueryException qe) {
            System.debug(LoggingLevel.ERROR, 'SOQL Syntax or Runtime Error: ' + qe.getMessage());
            // Rethrow custom application exception or log to persistent error framework
            throw new AuraHandledException('Invalid query parameter provided.');
        }
    }
}

3. Core Strategies to Prevent Query Exceptions

Core Rule: Never execute SOQL queries inside for loops, always assign queries to List collections to prevent cardinality errors, and use SOQL for-loops to process large datasets without blowing heap limits.
  • Bulkify Data Queries: Collect record IDs in Set<Id> collections and issue a single query using the WHERE Id IN :idSet operator outside loops.
  • SOQL For-Loops for Large Volumes: When processing thousands of records, iterate over queries in batches of 200 using for (List<Account> chunk : [SELECT Id FROM Account]) to prevent heap size exhaustion.
  • Ensure Query Selectivity: Filter by indexed fields (such as Id, Name, CreatedDate, External IDs, or Lookup fields) to avoid timeout exceptions on large data volumes.
  • Proactive Limits Monitoring: Use Limits.getQueries() and Limits.getLimitQueries() to inspect remaining headroom before executing dynamic queries in complex transactions.

4. Common Traps & Anti-Patterns

Governor Limit Trap: Uncatchable LimitExceptions
Unlike standard QueryException errors, a System.LimitException (such as exceeding 100 SOQL queries or 50,000 rows) cannot be caught using a standard try-catch block. The transaction terminates immediately and rolls back all uncommitted work. You must design code defensively using bulkification rather than relying on catch blocks.

Summary

Preventing query exceptions in Salesforce Apex requires disciplined architectural habits: avoiding single-record query assignments, ensuring query selectivity, bulkifying triggers, and strictly avoiding queries inside loops. By writing defensive SOQL and monitoring runtime limits, developers can ensure enterprise-grade reliability and performance across their Salesforce orgs.