Control Flow and Collections · Lesson 9 of 41
Loops in Apex: for, for-each, while and SOQL for Loops
Triggers receive up to 200 records at a time, batch jobs process millions, and integrations send lists. Loops are how you work through them. Apex has the familiar loops from Java plus one of its own – the SOQL for loop – and one rule that matters more than any syntax: never put a query or DML statement inside a loop.
In this lesson
- for, for-each, while and do-while
- Looping over Lists, Sets and Maps
- break and continue
- SOQL for loops
- Modifying a collection while looping
The classic for loop
List<Contact> newContacts = new List<Contact>();
for (Integer i = 1; i <= 3; i++) {
newContacts.add(new Contact(LastName = 'Learner ' + i));
}
System.debug(newContacts.size() + ' contacts built');
// counting down, stepping by 2
for (Integer i = 10; i > 0; i -= 4) {
System.debug('i = ' + i);
}
3 contacts built i = 10 i = 6 i = 2
for-each over Lists, Sets and Maps
List<String> stages = new List<String>{ 'Prospecting', 'Proposal', 'Closed Won' };
for (String stage : stages) {
System.debug('Stage: ' + stage);
}
Set<String> countries = new Set<String>{ 'India', 'Germany' };
for (String country : countries) {
System.debug('Country: ' + country);
}
Map<String, Integer> casesByPriority = new Map<String, Integer>{ 'High' => 4, 'Medium' => 9, 'Low' => 2 };
for (String priority : casesByPriority.keySet()) { // keys
System.debug(priority + ': ' + casesByPriority.get(priority));
}
Integer total = 0;
for (Integer n : casesByPriority.values()) total += n; // values
System.debug('Total cases: ' + total);
Stage: Prospecting Stage: Proposal Stage: Closed Won Country: India Country: Germany High: 4 Medium: 9 Low: 2 Total cases: 15
The for-each loop is what you'll use 90% of the time: it's shorter, and there's no index to get wrong. Iteration order of a List is its index order; Maps and Sets in Apex keep insertion order in practice, but don't build logic that depends on it.
while and do-while
// while: checks first – may run zero times
Integer attempts = 0;
Boolean delivered = false;
while (!delivered && attempts < 5) {
attempts++;
delivered = attempts == 3; // pretend the 3rd try succeeds
}
System.debug('Delivered after ' + attempts + ' attempts');
// do-while: runs at least once
Integer page = 0;
do {
page++;
System.debug('Fetching page ' + page);
} while (page < 2);
Delivered after 3 attempts Fetching page 1 Fetching page 2
Always make sure a while loop can end. An infinite loop in Apex doesn't hang forever – it hits the CPU time limit and the whole transaction fails.
break and continue
List<Decimal> invoiceAmounts = new List<Decimal>{ 1200, null, 560, -50, 9800, 300 };
Decimal sum = 0;
for (Decimal amount : invoiceAmounts) {
if (amount == null) continue; // skip this one, keep going
if (amount < 0) {
System.debug('Negative amount found – stopping');
break; // leave the loop completely
}
sum += amount;
}
System.debug('Sum before the bad value: ' + sum);
Negative amount found – stopping Sum before the bad value: 1760
SOQL for loops
A normal query loads all results into memory at once. A SOQL for loop streams them in chunks, which keeps the heap small when you process many records:
List<Account> seed = new List<Account>();
for (Integer i = 1; i <= 450; i++) seed.add(new Account(Name = 'Bulk ' + i));
insert seed;
// 1. one record at a time (the platform still fetches in chunks behind the scenes)
Integer count = 0;
for (Account a : [SELECT Id, Name FROM Account WHERE Name LIKE 'Bulk %']) {
count++;
}
System.debug('Records: ' + count);
// 2. a list of up to 200 records per iteration – ideal for bulk DML
Integer chunk = 0;
for (List<Account> batch : [SELECT Id, Name FROM Account WHERE Name LIKE 'Bulk %']) {
chunk++;
System.debug('Chunk ' + chunk + ': ' + batch.size() + ' records');
}
System.debug('Queries used: ' + Limits.getQueries());
Records: 450 Chunk 1: 200 records Chunk 2: 200 records Chunk 3: 50 records Queries used: 2
The list form hands you up to 200 records per iteration, so you can update each chunk with one DML statement. Each loop above is still one query. For really large volumes (hundreds of thousands of records) use Batch Apex.
Don't change a collection you're looping over
Adding to or removing from a List or Set inside a for-each loop over that same collection throws System.FinalException – and, as this run shows, it can't be caught: the transaction simply stops. Build a new collection instead (or loop over a copy):
List<String> tags = new List<String>{ 'apex', 'old', 'soql', 'old' };
// ✓ build a new list instead
List<String> kept = new List<String>();
for (String t : tags) {
if (t != 'old') kept.add(t);
}
System.debug('kept: ' + kept);
// ✗ removing from the list you're looping over
try {
for (String t : tags) {
if (t == 'old') tags.remove(tags.indexOf(t));
}
} catch (Exception e) {
System.debug('this catch block never runs');
}
System.debug('…and neither does this line');
kept: (apex, soql) System.FinalException: Cannot modify a collection while it is being iterated.
The golden rule: no SOQL or DML inside loops
A query inside a loop over 200 trigger records runs 200 queries – and the limit is 100. Query once before the loop (into a Map), and collect changes in a list to save once after it. This pattern is so important it has its own lesson: bulkification.
Practice
Try it yourself
Loop over a
Map<Id, Account>and print each account's name with its Id. Which collection method do you loop over?Show answer
for (Id accId : accountsById.keySet()) { System.debug(accId + ' ' + accountsById.get(accId).Name); }– or loop overvalues()if you only need the records.What's wrong here, and how do you fix it?
for (Contact c : contacts) { Account a = [SELECT Name FROM Account WHERE Id = :c.AccountId]; }Show answer
One query per contact – it will hit the 100-query limit. Collect the AccountIds in a Set, query once into a
Map<Id, Account>, then look up each contact's account in the loop.
Quick check
How many records does the list form of a SOQL for loop return per iteration?
Up to 200.
Which loop always runs at least once?
do-while.
What does continue do?
Skips the rest of the current iteration and moves to the next.
Summary
- Prefer for-each; use the classic for loop when you need an index or a step.
- Loop over Map keySet() or values(); use break and continue to control flow.
- SOQL for loops keep heap low; the list form gives 200-record chunks for bulk DML.
- Never modify a collection while iterating it, and never query or save inside a loop.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.