No. Salesforce blocks the call at runtime with:
System.AsyncException: Future method cannot be called from a future or batch method.
Future methods cannot start other future methods. The same rule blocks Batch Apex from invoking @future, and blocks future methods from calling out to Batch Apex.
The forbidden pattern
public class Forbidden {
@future
public static void step1(List<Id> ids) {
// ... do work
step2(ids); // AsyncException at runtime
}
@future
public static void step2(List<Id> ids) { /* ... */ }
}
The first future call works. The moment step1 calls step2, the platform throws.
Why the restriction exists
Future methods were Salesforce’s first async primitive. To prevent runaway chains that could exhaust async capacity (250,000 jobs/day), Salesforce capped chaining: one future per transaction, period. No depth, no fan-out.
When Queueable arrived in Winter ‘15, Salesforce relaxed the chaining model — but only inside Queueable’s own contract. Future stayed locked.
The two-future trap to know
| Caller | Callee | Allowed? |
|---|---|---|
| Sync code (controller, trigger) | @future | Yes |
@future | @future | No — AsyncException |
@future | Queueable | Yes |
Batch execute | @future | No — AsyncException |
Scheduled execute | @future | Yes (Scheduled is sync from the platform’s view) |
| Queueable | Queueable | Yes (one chained per execute) |
| Queueable | @future | Yes |
The workaround
Convert one (or both) future methods to Queueable. Queueable chaining is the modern path:
public class Step1 implements Queueable {
public void execute(QueueableContext ctx) {
// ... step 1 work
System.enqueueJob(new Step2());
}
}
public class Step2 implements Queueable {
public void execute(QueueableContext ctx) {
// ... step 2 work
}
}
Or call Queueable from inside the future:
@future
public static void step1(List<Id> ids) {
// ... step 1 work
System.enqueueJob(new Step2Queueable(ids));
}
Common interview follow-ups
- Is the error a compile-time or runtime error? — Runtime. The compiler doesn’t trace call chains.
- Can I unit-test this? — Yes — the AsyncException will surface inside
Test.startTest()/Test.stopTest(). - Why is Queueable allowed to chain but not future? — Different generation of the API. Queueable was designed with chaining as a first-class feature.
Verified against: Apex Developer Guide — Future Method Limitations. Last reviewed 2026-05-17.