[SalesForce] Future method in trigger causes batch failures

I've recently been asked to convert a method in our account trigger to a future method to help reduce CPU errors during execution. When attempting to run all test classes in my org (for a completely unrelated project), I found that the test classes for our batches that insert or update accounts started seeing this error:

caused by: System.AsyncException: Future method cannot be called from a future or batch method: class.method(Set<Id>)

That makes sense to me. Pretty straight forward. The update is happening in a batch and a future method is being called in the same context so, error.

What concerns me is that future methods in triggers are a common practice and batch api use is also common practice. This almost seems like it might be a good best practice to not use future methods in triggers at all as they essentially run the risk of breaking batches (or don't use batches).

The question is: What is the best practice (or is it even possible) to handle triggers asynchronously without impacting other asynchronous operations such as scheduled batches?

Best Answer

Check if you are already in an asynchronous process before calling your future method. Here's the basic idea sketched out:

public static class MyClass
{
    static Boolean shouldProcessAsync()
    {
        return !system.isFuture() && !system.isBatch() && !system.isQueueable() &&
            Limits.getLimitFutureCalls() > Limits.getFutureCalls();
    }

    public static void doStuff(List<MyObject__c> records)
    {
        if (records.isEmpty()) return;

        if (shouldProcessAsync())
        {
            doStuffAsync(new Map<Id, SObject>(records).keySet());
        }
        else
        {
            // logic
        }
    }
    @future
    static void doStuffAsync(Set<Id> recordIds)
    {
        doStuff([
            SELECT Name
            FROM MyObject__c
            WHERE Id IN :recordIds
        ]);
    }
}