flowable/flowable-engine · error · FlowableIllegalStateException

Invocation result + invocationResult + from invocation + inv

Error message

Invocation result + invocationResult + from invocation + invocation + was not a CompletableFuture

What it means

ServiceTaskFutureJavaDelegateActivityBehavior.execute throws FlowableIllegalStateException when a FutureJavaDelegate invocation returns a result that is not a CompletableFuture. Flowable plans the async completion (FutureJavaDelegateCompleteAction) on that future, so the contract requires every invocation to yield a CompletableFuture instance.

Source

Thrown at modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/ServiceTaskFutureJavaDelegateActivityBehavior.java:141

            try {
                if (loggingSessionEnabled) {
                    BpmnLoggingSessionUtil.addLoggingData(LoggingSessionConstants.TYPE_SERVICE_TASK_ENTER,
                                    "Executing service task with java class " + futureJavaDelegate.getClass().getName(), execution);
                }

                FutureJavaDelegate<Object> futureJavaDelegate = (FutureJavaDelegate<Object>) this.futureJavaDelegate;

                FutureJavaDelegateInvocation invocation = new FutureJavaDelegateInvocation(futureJavaDelegate, execution,
                        processEngineConfiguration.getAsyncTaskInvoker());
                processEngineConfiguration.getDelegateInterceptor().handleInvocation(invocation);

                Object invocationResult = invocation.getInvocationResult();
                if (invocationResult instanceof CompletableFuture) {
                    CompletableFuture<Object> future = (CompletableFuture<Object>) invocationResult;

                    CommandContextUtil.getAgenda(commandContext).planFutureOperation(future, new FutureJavaDelegateCompleteAction(futureJavaDelegate, execution, loggingSessionEnabled));
                } else {
                    throw new FlowableIllegalStateException(
                            "Invocation result " + invocationResult + " from invocation " + invocation + " was not a CompletableFuture");
                }

            } catch (RuntimeException e) {
                if (loggingSessionEnabled) {
                    BpmnLoggingSessionUtil.addErrorLoggingData(LoggingSessionConstants.TYPE_SERVICE_TASK_EXCEPTION,
                                    "Service task with java class " + futureJavaDelegate.getClass().getName() + " threw exception " + e.getMessage(), e, execution);
                }

                throw e;
            }

        } else {
            if (loggingSessionEnabled) {
                BpmnLoggingSessionUtil.addLoggingData(LoggingSessionConstants.TYPE_SKIP_TASK, "Skipped service task " + execution.getCurrentActivityId() +
                                " with skip expression " + skipExpressionText, execution);
            }
            if (!triggerable) {

View on GitHub (pinned to d6d39ce1c6)

Solutions

  1. Return a non-null CompletableFuture from the FutureJavaDelegate (wrap immediate values with CompletableFuture.completedFuture)
  2. Update delegates to the current FutureJavaDelegate interface after a Flowable upgrade
  3. Remove or fix code paths that return null early instead of an already-completed future
  4. If fully synchronous behavior is wanted, switch the class to implement JavaDelegate instead

Example fix

// before
public Object execute(DelegateExecution execution) {
  return lookup(execution); // plain value, not a future
}
// after
public CompletableFuture<Object> execute(DelegateExecution execution) {
  return CompletableFuture.completedFuture(lookup(execution));
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Test that the FutureJavaDelegate always returns a CompletableFuture
Object result = futureJavaDelegate.execute(execution);
if (!(result instanceof CompletableFuture)) {
    throw new IllegalStateException("FutureJavaDelegate must return a non-null CompletableFuture");
}

Type guard

public static boolean isCompletableFuture(Object o) {
    return o instanceof CompletableFuture;
}

Try / catch

try {
    processEngine.getRuntimeService().signal(executionId);
} catch (FlowableIllegalStateException e) {
    if (e.getMessage().contains("was not a CompletableFuture")) {
        // return CompletableFuture.completedFuture(...) instead
    }
}

Prevention

When it happens

Trigger: A FutureJavaDelegate's execute/invoke returns null or a plain object instead of CompletableFuture; the instanceof check on invocation.getInvocationResult() fails inside the service task execution path.

Common situations: Legacy FutureJavaDelegate implementations upgraded across Flowable versions (pre-async contract returned plain values); null returns on guard clauses; asynchronous code refactored to return a custom promise-like type instead of CompletableFuture.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11). Data as JSON: /api/errors/a25f169bd42c228d. Report an issue: GitHub.