Skip to main content
New tool CRON Expression Builder — preview next run times before you schedule Apex. Open the builder →
Diagram illustrating how to fix common Salesforce trigger recursion issues in Apex code
Apex

Fix Salesforce Trigger Recursion and Stack Depth Errors

Dealing with stack depth errors is a rite of passage for Salesforce developers. I will show you why these loops happen and how to use static variables to keep your triggers under control.

The short answer

Salesforce trigger recursion occurs when DML operations re-fire trigger logic in an endless loop, causing maximum stack depth errors. Developers can prevent these loops by filtering for actual field changes, using static ID sets for record-level control, or offloading heavy work to asynchronous Apex.

Key takeaways Compare Trigger.oldMap and Trigger.newMap values to ensure trigger logic only runs when relevant fields have actually changed. Use a static Set of IDs instead of a global static Boolean flag to maintain record-level recursion guards during bulk data processing. Clear static sets in Apex test classes between multiple DML statements to prevent false test failures. Offload resource-intensive or non-immediate record updates to Queueable or Future methods to run them in a separate transaction.

Why Salesforce trigger recursion happens

If you've spent any time in a complex org, you've probably run into the nightmare of Salesforce trigger recursion. It's that moment when your code updates a record, which fires a trigger, which updates the record again, and suddenly you're staring at a "Maximum stack depth reached" error. It's frustrating, but it's a common hurdle for every developer.

Look, it usually boils down to a few things. Maybe you're performing an update on the same object inside the trigger handler. Or maybe you have a chain reaction where Object A updates Object B, which then updates Object A again. In my experience, teams often lose days trying to untangle these loops because they didn't have a clear guard strategy from day one.

Common causes of the loop

  • Updating the same record in an after update trigger without checking if anything actually changed.
  • Cross-object updates that circle back to the original record.
  • Multiple triggers and workflows fighting for control over the same record.

A technical diagram illustrating a recursive loop between Salesforce records and triggers using directional arrows and system icons.

A technical diagram illustrating a recursive loop between Salesforce records and triggers using directional arrows and system icons.

Best ways to stop Salesforce trigger recursion

So how do we fix this? There isn't just one "right" way, but there are definitely some patterns that work better than others depending on your specific scenario. Let's break down the tools we have in the toolbox. The goal is to make your code predictable and safe for bulk operations.

1. The static Boolean flag (The simple guard)

This is the most common approach I see. Since static variables live for the entire transaction, you can use a Boolean to track if the trigger has already run. It's easy to set up, but honestly, it's a bit of a blunt instrument. It's great for simple logic, but it can cause issues in complex transactions.

public class TriggerHandler {
    public static Boolean isFirstRun = true;
}

trigger AccountTrigger on Account (after update) {
    if (TriggerHandler.isFirstRun) {
        TriggerHandler.isFirstRun = false;
        // Your logic here
    }
}

But here's the thing: this can actually bite you. If you're processing 200 records and then another 200 in the same transaction, that second batch might get skipped entirely. I've seen this cause data issues in bulk uploads, so use it with caution and only when you know the transaction lifecycle inside and out.

2. Using a Set of IDs for record-level control

If you need something more precise, a Set of IDs is the way to go. Instead of blocking the whole trigger, you only block the specific records that have already been processed. This is much safer for bulk operations and is usually what I recommend for enterprise-level orgs.

Pro tip: Always clear your static sets in your test classes if you're running multiple DML statements in one test method, or your tests might fail for the wrong reasons.

public class AccountState {
    public static Set<Id> processedIds = new Set<Id>();
}

trigger AccountTrigger on Account (after update) {
    List<Account> toProcess = new List<Account>();
    for (Account acc : Trigger.new) {
        if (!AccountState.processedIds.contains(acc.Id)) {
            toProcess.add(acc);
        }
    }
    
    if (toProcess.isEmpty()) return;
    
    // Logic here...
    AccountState.processedIds.addAll(toProcess.keySet());
}

3. Check for actual field changes

One thing that trips people up is running logic even when the relevant fields haven't changed. Before you do any DML, compare Trigger.oldMap with Trigger.newMap. If the "Status" field didn't change, why are you running the "Status Update" logic? This simple check prevents a lot of unnecessary Salesforce trigger recursion.

If you're still deciding between using code or a declarative tool, you might want to check out my guide on Apex vs Flow. Sometimes a Flow can handle the logic without the same recursion headaches, but code gives you more control over the state when things get messy.

4. Move work to Asynchronous Apex

Sometimes the best way to avoid a loop is to just step out of the current transaction. By using a Queueable or a Future method, you start a fresh transaction with its own limits. This is a solid move for heavy processing that doesn't need to happen instantly, like calling an external API or updating thousands of related records.

We've talked before about how to manage Asynchronous Apex, and it's a lifesaver here. Just remember that you can't fire a Future method from another Future method, so you still need to plan your architecture carefully to avoid hitting different limits.

Key Takeaways

  • Salesforce trigger recursion happens when DML operations re-fire the same trigger logic in a loop.
  • Static Booleans are simple to write but can accidentally skip records during bulk processing.
  • Static Sets of IDs are the gold standard for record-level recursion protection.
  • Always compare old and new field values to see if the update is even necessary before running logic.
  • Consider moving logic to a Salesforce Apex Trigger handler class to keep your code organized and testable.

Handling recursion isn't just about stopping errors; it's about making your org predictable. When I first started, I thought a single Boolean was enough for everything. I quickly learned that as an org grows, you need a more nuanced approach. Start with field-level checks, move to ID sets when needed, and always keep an eye on your debug logs to see exactly how many times your code is firing. It'll save you a lot of stress during your next big deployment.

Frequently asked questions

How do you prevent trigger recursion in Salesforce?

You can prevent recursion by comparing old and new field maps to ensure values changed, maintaining a static Set of processed record IDs, or delegating operations to asynchronous Apex.

Why can a static Boolean flag cause issues in bulk Salesforce triggers?

A static Boolean lives for the entire transaction, so setting it to false on the first execution can cause subsequent batches of 200 records in the same bulk operation to be skipped entirely.

Why does Salesforce throw a Maximum stack depth reached error?

This error occurs when code enters an unmanaged recursive loop, such as an after update trigger repeatedly updating the same object, cross-object updates circling back to the original record, or conflicting automated workflows.

Why should you clear static ID sets in Apex test classes?

Static sets retain their stored record IDs across multiple DML statements in the same test method, which can unintentionally block records from processing in subsequent operations and cause tests to fail.

Newsletter

One email every Tuesday

New guides, tool updates, and the release-note changes that break things.

No spam. Unsubscribe in one click.

Comments

Loading comments...

Leave a Comment