System.ListException: List index out of bounds: <N>
You indexed a `List` past its size. Apex doesn't return null for out-of-range — it throws. The most common pattern is `[SELECT ... LIMIT 1][0]` against an empty list.
Also seen asList index out of bounds·ListException·System.ListException: List index·list out of bounds apex
Apex lists are zero-indexed and bounded. myList[5] requires myList.size() > 5. Otherwise: ListException: List index out of bounds.
The classic offender
Account a = [SELECT Id FROM Account WHERE Name = 'Acme' LIMIT 1][0];
If no Account named Acme exists, the SOQL returns an empty list. [0] throws.
The right pattern:
List<Account> hits = [SELECT Id FROM Account WHERE Name = 'Acme' LIMIT 1];
if (hits.isEmpty()) return null; // or throw a meaningful error
Account a = hits[0];
Always check size before indexing. The isEmpty() / size() check costs nothing.
Closely related but different
System.QueryException: List has no rows for assignment to SObject (see that page) fires when you assign a SOQL result directly to an SObject variable and the result is empty. ListException fires when you index a List variable past its end. Different exception type, same family of bug.
When the index is computed
for (Integer i = 0; i <= myList.size(); i++) { // ❌ off-by-one
process(myList[i]);
}
The condition i <= size() runs one iteration past the end. Fix to < not <=. Or use the foreach form, which can't be off-by-one:
for (Object item : myList) process(item);
A subtle source: parallel arrays
Code that maintains two lists in parallel (e.g., keys and values indexed together) breaks the moment one list grows differently from the other:
List<Id> keys = new List<Id>();
List<String> values = new List<String>();
// ... populate ...
for (Integer i = 0; i < keys.size(); i++) {
String v = values[i]; // 💥 if values.size() < keys.size()
}
Replace with a Map<Id, String> whenever the two lists should stay in sync. Maps are inherently consistent.
The defensive test
Add a unit test that exercises the empty-list path explicitly:
@isTest static void emptyResultIsHandledGracefully() {
// Org has no record matching the filter
Account a = MyClass.findByName('NoSuchRecord');
System.assertEquals(null, a, 'Empty result should return null, not throw');
}
The test that runs against a populated sandbox passes; the one that runs against an empty test context catches the bug before production.
