Query into a List and check isEmpty() before you touch the record. Single-SObject assignment is a language feature that asserts exactly one row exists — use it only where zero rows genuinely is a bug worth throwing on. If you hit this in a test, the query is fine and the test data is missing: SeeAllData=false is the default, so nothing exists that your @testSetup did not create.
What the error means, mechanically
Every SOQL query in Apex returns a list. When you write Account a = [SELECT ...], the compiler inserts an implicit conversion from List<Account> to Account. That conversion has two failure modes and both throw System.QueryException at runtime:
- Zero rows → "List has no rows for assignment to SObject".
- More than one row → "List has more than 1 row for assignment to SObject".
The same implicit conversion applies when you access a field straight off a query expression — String n = [SELECT Name FROM Account WHERE ...].Name; — so that line throws for the same reason, with the same message.
This is not a null-safety problem. There is no null to check for: the exception is thrown by the assignment itself, before your variable ever exists.
The causes, ranked by how often they actually happen
1. A test with no test data (by far the most common)
Apex tests run with SeeAllData=false unless you explicitly opt out, which means the test transaction starts with an empty database — no Accounts, no Products, no Custom Settings records, none of the reference data sitting in your sandbox. Code that works in the org fails the moment it runs under test, on the first query that assumed something existed.
// Fails under test: no CustomSetting row exists
public class PricingService {
public static Decimal getMargin() {
Pricing_Config__c cfg = [SELECT Margin__c FROM Pricing_Config__c LIMIT 1];
return cfg.Margin__c;
}
}
// The test that surfaces it
@isTest
static void testMargin() {
// No @testSetup, no insert — Pricing_Config__c is empty
Test.startTest();
Decimal m = PricingService.getMargin(); // QueryException here
Test.stopTest();
}Fix the test by creating the data, and fix the service by not assuming:
@isTest
private class PricingServiceTest {
@testSetup
static void setup() {
insert new Pricing_Config__c(Margin__c = 0.15);
}
@isTest
static void testMargin() {
System.assertEquals(0.15, PricingService.getMargin());
}
}
// And make the service explicit about the empty case
public static Decimal getMargin() {
List<Pricing_Config__c> rows = [SELECT Margin__c FROM Pricing_Config__c LIMIT 1];
return rows.isEmpty() ? DEFAULT_MARGIN : rows[0].Margin__c;
}A related trap: @testSetup data is created once and rolled back to that snapshot between test methods, but it does not run at all if the class has no @testSetup method — and a @testSetup method that itself throws leaves every method in the class failing on the first query.
2. The WHERE clause doesn't match what you think it matches
Second most common, and the most annoying to spot. Usual suspects: filtering a picklist by its label when the API name differs, a trailing space in a string literal, a date field compared against a DateTime, or a filter on a field the record has not been populated with yet.
// Label is "In Progress"; API name is "InProgress"
Case c = [SELECT Id FROM Case WHERE Status = 'In Progress' LIMIT 1];
// Correct
Case c = [SELECT Id FROM Case WHERE Status = 'InProgress' LIMIT 1];3. The record exists but this context can't see it
Records inserted in the same transaction are visible to later queries in that transaction, but several boundaries break that assumption:
- Async work. A record enqueued for insert inside a future/queueable/batch is not visible until that job runs — in tests, not until
Test.stopTest(). - Sharing. A
with sharingclass running as a low-privilege user inSystem.runAsmay filter out rows a system-context test just created. - Deleted rows. SOQL excludes
IsDeleted = true; a record sent to the Recycle Bin stops matching without disappearing from the org.
4. A hardcoded Id that isn't in this org
WHERE Id = '0015g00000XXXXX' works in the sandbox it was copied from and nowhere else. Query the record by a stable business key, or create it in the test.
5. Single-row assignment used where multiple rows were always possible
Sometimes the query is right and the pattern is wrong. If zero rows is a legitimate business state, the assertion built into single-SObject assignment is the bug.
The three safe patterns
// 1. List + isEmpty — the default. Zero rows is a normal outcome.
List<Account> accts = [SELECT Id, Name FROM Account WHERE Name = :name LIMIT 1];
if (accts.isEmpty()) {
return null; // or a default, or a domain-specific exception
}
Account a = accts[0];
// 2. Map keyed by Id — for the "look it up by Id" case
Map<Id, Account> byId = new Map<Id, Account>(
[SELECT Id, Name FROM Account WHERE Id IN :accountIds]
);
Account a = byId.get(someId); // null instead of throwing
// 3. Keep the throw, but make it say something useful
List<Account> rows = [SELECT Id FROM Account WHERE Name = :name];
if (rows.isEmpty()) {
throw new ConfigurationException('No Account named ' + name + ' — check setup data.');
}Pattern 3 matters more than it looks. "List has no rows for assignment to SObject" tells a future reader nothing about which query failed or what was expected. One line of explicit throwing turns a 20-minute investigation into a 20-second one.
Reproduce it locally, and see the query that returned nothing
Nimbus runs your Salesforce Apex tests on your machine against an embedded PostgreSQL loaded with your project's schema, so this exception reproduces in seconds without a deploy. It throws the same System.QueryException with the same message, so what you fix locally is what the org sees.
The useful part is the diagnosis. nimbus explain prints the failure alongside the SOQL and DML that ran immediately before it, each with its row count — so you see the query that returned zero rows and the inserts that were supposed to populate it, instead of inferring both from a stack trace.
# Run the failing test locally
nimbus test "PricingServiceTest.testMargin"
# Then ask why it failed — exception, location, and the SOQL/DML that preceded it
nimbus explain PricingServiceTest.testMargin