System.AsyncException: Future method cannot be called from a future or batch method
You called an `@future` method from inside another `@future`, batch, or queueable. Salesforce blocks recursive async chains because they could spiral into infinite job creation. The replacement is `Queueable` Apex, which can chain itself.
Also seen asAsyncException: Future method cannot be called·future from future·future from batch·Future method cannot be called from a future or batch method
A batch job processes new accounts, and for each one calls an @future(callout=true) method that pings an external system. It worked when batch-size was 1; now the batch runs with batch-size 200 and dies with System.AsyncException: Future method cannot be called from a future or batch method. The async pattern that used to be the textbook answer for "do this work later" is the wrong tool for this context.
What Salesforce refuses
Apex provides three async-execution mechanisms: @future methods, Queueable Apex, and Batch Apex. Each runs in its own execution context with separate governor budgets. The platform restricts which contexts can call which others to prevent runaway recursion.
The specific rule that produces this error: @future methods cannot be invoked from inside another @future method, a Batch Apex execute() invocation, or a queueable's execute() invocation. The platform refuses these chains at runtime with the exception you're seeing.
The reason is operational. If @future could chain to @future without limit, a misbehaving piece of code could enqueue millions of future calls in a few seconds, exhausting the org's async-execution capacity for hours. The restriction is a safety brake.
Three things you can do in an async context:
- Call non-async Apex methods.
- Enqueue a Queueable Apex job (with caveats discussed below).
- Schedule a Batch Apex job (via
Database.executeBatch, with limits).
What you can't do:
- Call another
@futuremethod. - Make synchronous callouts (this is a different cap, not the one we're discussing).
The broken example
A batch processor that fires off per-record async notifications:
public class AccountBatchProcessor implements Database.Batchable<sObject> {
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator('SELECT Id, Name, OwnerEmail FROM Account WHERE Needs_Notification__c = TRUE');
}
public void execute(Database.BatchableContext bc, List<Account> scope) {
for (Account a : scope) {
// Throws AsyncException: Future method cannot be called from a future or batch method.
NotificationService.sendEmailAsync(a.Id);
}
}
public void finish(Database.BatchableContext bc) {}
}
public class NotificationService {
@future(callout=true)
public static void sendEmailAsync(Id accountId) {
// ... HTTP callout + email send ...
}
}
The intent is to defer the email and callout work so each batch record doesn't block. The execution refuses because batch execute() is itself async, and Salesforce won't let it chain to @future.
The fix: use Queueable Apex instead
Queueable Apex was designed to replace @future in async-chain scenarios. A queueable can:
- Be enqueued from synchronous code,
@futuremethods, batchexecute(), and other queueables. - Chain itself: a queueable can enqueue another queueable from its own
execute(). - Carry state across the chain (queueable classes are full Apex classes, not just static methods).
- Make HTTP callouts (when implementing
Database.AllowsCallouts).
The translation from the broken example:
public class AccountBatchProcessor implements Database.Batchable<sObject> {
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator('SELECT Id, Name FROM Account WHERE Needs_Notification__c = TRUE');
}
public void execute(Database.BatchableContext bc, List<Account> scope) {
Set<Id> accountIds = new Set<Id>();
for (Account a : scope) accountIds.add(a.Id);
// Queueable can be enqueued from batch execute; @future cannot.
System.enqueueJob(new NotificationQueueable(accountIds));
}
public void finish(Database.BatchableContext bc) {}
}
public class NotificationQueueable implements Queueable, Database.AllowsCallouts {
private Set<Id> accountIds;
public NotificationQueueable(Set<Id> ids) {
this.accountIds = ids;
}
public void execute(QueueableContext qc) {
for (Account a : [SELECT Id, Name, OwnerEmail FROM Account WHERE Id IN :accountIds]) {
NotificationService.sendOne(a);
}
}
}
The batch enqueues one queueable per execute() invocation. The queueable does the work in its own fresh governor context. The chain is legal.
The fixed example, with chaining
For very large queues, the queueable can chain itself:
public class NotificationQueueable implements Queueable, Database.AllowsCallouts {
private List<Id> remaining;
private static final Integer CHUNK_SIZE = 50;
public NotificationQueueable(List<Id> ids) {
this.remaining = ids;
}
public void execute(QueueableContext qc) {
Integer toProcess = Math.min(CHUNK_SIZE, remaining.size());
List<Id> chunk = new List<Id>();
for (Integer i = 0; i < toProcess; i++) chunk.add(remaining[i]);
for (Account a : [SELECT Id, Name, OwnerEmail FROM Account WHERE Id IN :chunk]) {
NotificationService.sendOne(a);
}
// Chain to the next chunk if there's more work.
List<Id> nextRemaining = new List<Id>(remaining);
for (Integer i = 0; i < toProcess; i++) nextRemaining.remove(0);
if (!nextRemaining.isEmpty() && !Test.isRunningTest()) {
System.enqueueJob(new NotificationQueueable(nextRemaining));
}
}
}
Each link in the chain handles 50 records. After processing, it enqueues the next link if more remains. The pattern scales to thousands of records, each chunk gets its own governor budget, and there's no async-from-async violation.
The Test.isRunningTest() guard avoids infinite chains in tests, where you usually want to verify just the first invocation.
When the error fires from @future calling @future
The same exception fires if one @future method calls another:
@future
public static void parentFuture() {
childFuture(); // Throws AsyncException
}
@future
public static void childFuture() {
// Work
}
The fix is structurally identical: refactor to Queueable, which supports chaining. The parent enqueues the child instead of calling it directly.
What the platform allows in async contexts
A practical chart for "from context X, can I call context Y":
| From / Calling | Sync method | @future | Queueable | Batch |
|---|---|---|---|---|
| Sync | OK | OK | OK | OK |
| @future | OK | No (this error) | OK | OK |
| Queueable.execute() | OK | No (this error) | OK | OK |
| Batch.execute() | OK | No (this error) | OK | No (one per batch) |
The "from queueable to batch" cell is sometimes a surprise: you can do it, but only one batch can be enqueued per queueable invocation. Multiple batches require chaining queueables.
A subtle case: @future from a trigger
Triggers run synchronously by default, so a trigger that calls @future is fine in the typical case. The error fires only if the trigger itself was invoked from an async context.
The most common async-from-trigger path: a batch execute() updates records, the records' trigger fires, and the trigger calls @future. The trigger is technically running in the batch's async context, so the @future call fails.
The fix is the same: replace the trigger's @future with System.enqueueJob(new SomeQueueable(...)). The queueable inherits the appropriate context and works in both sync and async trigger paths.
Test patterns that catch this
A test that exercises the async-call path of a trigger or class:
@isTest
static void notificationService_doesNotFailFromBatch() {
List<Account> accounts = new List<Account>();
for (Integer i = 0; i < 5; i++) {
accounts.add(new Account(Name = 'Test ' + i, Needs_Notification__c = true));
}
insert accounts;
Test.startTest();
Database.executeBatch(new AccountBatchProcessor(), 5);
Test.stopTest();
// Queueables enqueued during Test.startTest/stopTest are executed by stopTest.
// The test asserts that the batch + queueable chain completes without exception.
Integer queuedJobs = [SELECT COUNT() FROM AsyncApexJob WHERE JobType = 'Queueable'];
System.assert(queuedJobs > 0, 'At least one queueable should have run');
}
The test confirms that the batch enqueues queueables without throwing. If you accidentally regress to calling @future from batch, this test fails immediately.
What the design rationale tells you
The platform's restriction is a hint that the right primitive for "do this later" is Queueable, not @future. New code should prefer Queueable across the board:
- It works in every context.
- It supports state via the class's instance fields.
- It supports chaining for unbounded work.
- It supports HTTP callouts via
Database.AllowsCallouts. - It's easier to debug because each enqueue produces a discoverable
AsyncApexJobrow.
@future remains useful for the simplest "fire and forget" case from synchronous code where you don't need state or chaining. For everything else, Queueable wins.
The platform's overall async budget
Each Apex transaction can enqueue up to 50 queueable jobs, 50 @future calls, and one batch. Queueables that chain to other queueables count individually toward the 50.
For very high-volume async work that needs many independent chains, the architectural answer is Batch Apex: one batch can process millions of records by chunking them, each chunk gets its own governor budget, and the batch itself counts as one async invocation against the calling transaction's budget.
Why Queueables came after @future
@future was the original async mechanism in Apex (introduced around 2010). Its design was minimal: a static method annotation that defers execution. The restrictions around chaining were added later as the platform's async-execution capacity scaled.
Queueable was introduced in Spring '15 to address @future's limitations. Its design borrows from the JavaScript Promise pattern: an object with an execute method that runs in the platform's async queue. The capabilities it added (chaining, state, callouts, instance variables) are what most modern async code needs.
New codebases should treat Queueable as the default and @future as legacy. Old code that uses @future is fine, but every new async path should reach for Queueable first. The future-from-future error class disappears as a side effect.
A workaround pattern for code you can't immediately refactor
Sometimes you have an existing @future method that gets called from many places, and one of those callers now runs from an async context. The full refactor takes time. A short-term mitigation:
public static void doWorkAsync(Id recordId) {
if (System.isFuture() || System.isQueueable() || System.isBatch()) {
// Already in an async context. Enqueue a queueable instead.
System.enqueueJob(new DoWorkQueueable(recordId));
} else {
// Sync context, the original @future is fine.
DoWorkService.doWork_future(recordId);
}
}
The dispatcher chooses the right async path based on the calling context. Old callers keep working; new async callers get Queueable. The full refactor can happen on a separate timeline.
System.isFuture(), System.isQueueable(), and System.isBatch() are platform-provided context checks that return true only when called from within the corresponding async type.
Monitoring async chains
For production async pipelines, the AsyncApexJob table is your friend. Query it to see what's running, what's queued, and what failed:
[
SELECT Id, JobType, Status, JobItemsProcessed, NumberOfErrors, ApexClass.Name
FROM AsyncApexJob
WHERE CreatedDate = LAST_N_DAYS:1
ORDER BY CreatedDate DESC
LIMIT 50
]
Build a Lightning report over the table. Alert when the failure count rises above a baseline. Chain failures (queueable A enqueued B but B never ran) show up as gaps in the timeline.
Per-context governor budgets
Each async context starts with a fresh budget for most governors. Queries, DML, CPU time, heap size all reset when you cross into a new async context.
The values to remember:
- Synchronous transaction: 100 SOQL queries, 150 DML statements, 10,000 DML rows, 6 million microseconds CPU.
- Async transaction (including Queueable, Batch execute, @future): 200 SOQL queries, 150 DML statements, 10,000 DML rows, 60 million microseconds CPU.
The doubling of SOQL and the 10x of CPU in async contexts is a significant difference. Work that bumps the synchronous CPU cap often fits comfortably in an async context. Architecturally, "move it async" is often the right answer for synchronous-cap-related governor issues.
The trade-off is latency: async runs whenever the platform schedules it, not immediately. For user-facing operations where the result must appear instantly, async isn't an option. For background work, the latency is usually acceptable.
A subtle observation about test transactions
Apex tests can call @future, Queueable, and Batch directly. The platform queues the async work but doesn't execute it until Test.stopTest() is called. That's when the queued jobs actually run, synchronously, within the test transaction. After Test.stopTest, you can query the database to see the side effects.
A common mistake: putting assertions before Test.stopTest(). The async work hasn't run yet, so assertions about its side effects fail. The pattern is always:
Test.startTest();
// Trigger the work
Test.stopTest();
// Assert side effects here
Place every async-asserting test query after Test.stopTest(). The pattern is uniform across @future, Queueable, and Batch.
When mixing async types feels right but isn't
A design that "looks correct" but produces the future-from-future error: a queueable that needs to defer an HTTP callout to a separate context. The intuition is to use @future(callout=true) for the callout. The platform refuses; the fix is to make the queueable itself implement Database.AllowsCallouts.
public class WorkQueueable implements Queueable, Database.AllowsCallouts {
public void execute(QueueableContext qc) {
// HTTP callout works because the queueable implements AllowsCallouts.
Http http = new Http();
HttpRequest req = new HttpRequest();
// ... configure ...
HttpResponse res = http.send(req);
}
}
No separate @future is needed. The queueable does callouts itself.
Further reading from Salesforce
Related dictionary terms
Share this fix
Related Apex errors
Initial term of field expression must be a concrete SObject: <type>
ApexYou wrote `someThing.Field__c` where `someThing` isn't a specific SObject type — usually an `SObject` or `Object` reference. Apex needs the …
Method does not exist or incorrect signature
ApexThe Apex compiler can't find a method matching the call you wrote — wrong name, wrong argument types, or wrong number of arguments. The comp…
MIXED_DML_OPERATION: DML operation on setup object is not allowed after you have updated a non-setup object (or vice versa)
ApexSalesforce splits objects into "setup" (User, Group, GroupMember, PermissionSet, Profile, Queue, etc.) and "non-setup" (everything else). A …
System.CalloutException: Read timed out
ApexThe HTTP callout exceeded its allowed read time waiting for the remote server's response. Salesforce caps a single callout at 120 seconds (d…
System.DmlException: Insert failed (or Update / Upsert / Delete failed)
ApexA DML statement (insert/update/upsert/delete) failed. The exception message contains a "first exception on row N" line that tells you which …