elsa-workflows/elsa-core · error · InvalidOperationException

The method must return Task or ValueTask

Error message

The {methodInfo.Name} method must return Task or ValueTask

What it means

After locating the middleware's Invoke/InvokeAsync method, GetInvokeMethod validates that it returns Task or ValueTask, because the pipeline awaits middleware via reflection. This error is thrown when the middleware entry-point method returns something else (void, sync result, custom type), which would break the awaitable pipeline contract.

Solutions

  1. Change the middleware method's return type to Task (use async/await and return Task) or ValueTask.
  2. If no async work exists, return Task.CompletedTask from a Task-returning method instead of void.
  3. Wrap existing sync logic: `public async ValueTask InvokeAsync(Ctx ctx, Del next) { DoWork(); await next(ctx); }`.
  4. Ensure the method name is exactly Invoke or InvokeAsync and it is a public instance method.

Example fix

// before
public void InvokeAsync(CommandContext context, CommandMiddlewareDelegate next)
{
    next(context);
}

// after
public async ValueTask InvokeAsync(CommandContext context, CommandMiddlewareDelegate next)
{
    await next(context);
}
Defensive patterns

Strategy: validation

Validate before calling

// Validate the middleware entry point returns Task or ValueTask before registering:
var invoke = middlewareType.GetMethods(BindingFlags.Instance | BindingFlags.Public)
    .FirstOrDefault(m => m.Name is "Invoke" or "InvokeAsync");
if (invoke != null &&
    !typeof(Task).IsAssignableFrom(invoke.ReturnType) &&
    !typeof(ValueTask).IsAssignableFrom(invoke.ReturnType))
    throw new InvalidOperationException($"{middlewareType.Name}.{invoke.Name} must return Task or ValueTask.");

Type guard

static bool IsAwaitableMiddleware(Type t)
{
    var m = t.GetMethods(BindingFlags.Instance | BindingFlags.Public)
             .FirstOrDefault(x => x.Name is "Invoke" or "InvokeAsync");
    return m != null && (typeof(Task).IsAssignableFrom(m.ReturnType) || typeof(ValueTask).IsAssignableFrom(m.ReturnType));
}

Try / catch

try
{
    pipeline.AddMiddleware(typeof(MyMiddleware));
}
catch (InvalidOperationException ex) when (ex.Message.Contains("must return Task or ValueTask"))
{
    logger.LogError(ex, "{Type} middleware entry point is not awaitable", typeof(MyMiddleware).Name);
    throw;
}

Prevention

When it happens

Trigger: Writing `public void InvokeAsync(...)` or `public string Invoke(...)`; returning a plain value or a non-awaitable custom wrapper type; using an older sync middleware signature in the async pipeline; a method named InvokeAsync that returns a custom builder type instead of Task/ValueTask.

Common situations: Porting synchronous middleware from another mediator library; writing middleware with side effects only and instinctively returning void; auto-generating middleware stubs with wrong return types; generic wrappers returning Task<T>-derived custom types that no longer satisfy the direct Task/ValueTask check.

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 elsa-workflows/elsa-core@fe9217bdfa (2026-09-13). Data as JSON: /api/errors/7292f3b99757894a. Report an issue: GitHub.

Appendix: source

Thrown at src/common/Elsa.Mediator/Middleware/MiddlewareHelpers.cs:34

    public static MethodInfo GetInvokeMethod(Type middleware)
    {
        const string invokeMethodName = "Invoke";
        const string invokeAsyncMethodName = "InvokeAsync";
        var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public);
        var invokeMethods = methods.Where(m => string.Equals(m.Name, invokeMethodName, StringComparison.Ordinal) || string.Equals(m.Name, invokeAsyncMethodName, StringComparison.Ordinal)).ToArray();

        switch (invokeMethods.Length)
        {
            case > 1:
                throw new InvalidOperationException("Multiple Invoke methods were found. Use either Invoke or InvokeAsync.");
            case 0:
                throw new InvalidOperationException("No Invoke methods were found. Use either Invoke or InvokeAsync");
        }

        var methodInfo = invokeMethods[0];

        if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType) && !typeof(ValueTask).IsAssignableFrom(methodInfo.ReturnType))
            throw new InvalidOperationException($"The {methodInfo.Name} method must return Task or ValueTask");

        return methodInfo;
    }
}

View on GitHub (pinned to fe9217bdfa)