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
The platform refuses to start a @future from any context that's already running asynchronously. The reason is straightforward: @future jobs are queued, and a chain of @future → @future → @future would let one user request spawn arbitrarily many jobs in the system queue. The hard rule prevents that whole class of runaway.
What you probably want instead
Almost every "I need to call a future from a future" turns into Queueable Apex. Queueables are the modern async primitive — they chain themselves explicitly, return a job ID you can monitor, and run within the 60-second async governor budget.
public class StepA implements Queueable {
public void execute(QueueableContext ctx) {
// do work
System.enqueueJob(new StepB()); // chain into the next stage
}
}
public class StepB implements Queueable {
public void execute(QueueableContext ctx) {
// next phase
}
}
enqueueJob from inside a Queueable's execute() is allowed and returns the new job's Id. You can build long pipelines this way — A → B → C — without hitting AsyncException.
The chain limits to know
- From Queueable: you can chain to one more Queueable (
enqueueJob). In production orgs the chain is unbounded; tests are limited to two levels deep without@isTest(SeeAllData=true)tricks. - From Batch Apex: you can call
enqueueJobfromfinish()to start a Queueable. You cannot call@futurefrom any Batch method. - From a
@futuremethod: you cannot call any other async work, period. No@future, noenqueueJob, noDatabase.executeBatch. - From a Scheduled Apex job: you can launch Batch or Queueable from
execute().
Migrating an existing @future chain
The mechanical refactor:
// Before
@future public static void step1(Set<Id> ids) {
// ... work ...
step2(ids); // throws AsyncException
}
@future public static void step2(Set<Id> ids) { ... }
// After
public class Step1 implements Queueable {
public Set<Id> ids;
public Step1(Set<Id> ids) { this.ids = ids; }
public void execute(QueueableContext ctx) {
// ... work ...
System.enqueueJob(new Step2(ids));
}
}
public class Step2 implements Queueable { /* ... */ }
Callers that did MyClass.step1(ids) now do System.enqueueJob(new Step1(ids)). The behaviour is the same; the chain works.
When you really need @future
@future(callout=true) is still the easiest way to fire-and-forget an HTTP callout from a trigger. If you must, do the future call directly from the trigger (synchronous origin), not from another async context.
