@future method calls another @future method

ID

apex.correctness.future_calls_future

Severity

high

Remediation Complexity

medium

Remediation Risk

medium

Remediation Effort

medium

Resource

Api Usage

Language

Apex

Description

Reports a @future method whose body calls another @future method declared in the same class, plainly or through a qualified reference. Only same-class calls are reported: OpenGrep does not resolve a callee across files, so a @future method calling a @future method declared elsewhere is not detected.

Rationale

A @future method already runs in an asynchronous transaction, and the platform refuses to start another one from inside it. The call throws AsyncException - "Future method cannot be called from a future or batch method" - at runtime, not at compile time, so the class deploys and the failure appears only when the asynchronous job actually runs. That is the worst place for it: nothing is on screen, the user has already been told the operation succeeded, and the only trace is a failed job in the Apex Jobs list or an email nobody is watching. Whatever the second method was to do is silently never done.

The following code illustrates the pattern detected by this rule:

public class InvoiceDispatcher {

    @future
    public static void enqueue(Id invoiceId) {
        System.debug(LoggingLevel.INFO, invoiceId);
    }

    @Future
    public static void archive(Id invoiceId) {
        System.debug(LoggingLevel.INFO, invoiceId);
    }

    // FLAGGED: @future method calls another @future method
    @future
    public static void dispatch(Id invoiceId) {
        enqueue(invoiceId);
    }

    // FLAGGED: @future method calls another @future method
    @future(callout=true)
    public static void send(Id invoiceId) {
        InvoiceDispatcher.archive(invoiceId);
    }

    @future
    public static void stamp(Id invoiceId) {
        String label = describe(invoiceId);
        System.debug(LoggingLevel.INFO, label);
    }

    public static String describe(Id invoiceId) {
        return 'invoice-' + invoiceId;
    }

    public static void dispatchAll(Set<Id> invoiceIds) {
        for (Id i : invoiceIds) {
            System.debug(LoggingLevel.FINE, i);
        }
        enqueue(null);
    }
}

Remediation

Do the second method’s work inline in the first, or move both steps into a Queueable and chain them with System.enqueueJob, which is the supported way to run one asynchronous step after another. Where the second step is a callout, @future(callout=true) on the single method does what the chain was trying to achieve.

Configuration

This detector does not need any configuration.

© 2026 Xygeni. All rights reserved