lysine-dev/retrofit · error · IllegalStateException

CompletableFuture return type must be parameterized as Compl

Error message

CompletableFuture return type must be parameterized as CompletableFuture<Foo> or CompletableFuture<? extends Foo>

What it means

Retrofit's Java 8 adapter handles methods returning java.util.concurrent.CompletableFuture, but only when the return type is parameterized with an element type. A raw CompletableFuture has no type information, so Java8CallAdapterFactory cannot determine the body type to deserialize and throws IllegalStateException during call-adapter resolution.

Source

Thrown at retrofit-adapters/java8/src/main/java/retrofit2/adapter/java8/Java8CallAdapterFactory.java:67

 *           errors
 *     </ul>
 */
@Deprecated
public final class Java8CallAdapterFactory extends CallAdapter.Factory {
  public static Java8CallAdapterFactory create() {
    return new Java8CallAdapterFactory();
  }

  private Java8CallAdapterFactory() {}

  @Override
  public @Nullable CallAdapter<?, ?> get(
      Type returnType, Annotation[] annotations, Retrofit retrofit) {
    if (getRawType(returnType) != CompletableFuture.class) {
      return null;
    }
    if (!(returnType instanceof ParameterizedType)) {
      throw new IllegalStateException(
          "CompletableFuture return type must be parameterized"
              + " as CompletableFuture<Foo> or CompletableFuture<? extends Foo>");
    }
    Type innerType = getParameterUpperBound(0, (ParameterizedType) returnType);

    if (getRawType(innerType) != Response.class) {
      // Generic type is not Response<T>. Use it for body-only adapter.
      return new BodyCallAdapter<>(innerType);
    }

    // Generic type is Response<T>. Extract T and create the Response version of the adapter.
    if (!(innerType instanceof ParameterizedType)) {
      throw new IllegalStateException(
          "Response must be parameterized" + " as Response<Foo> or Response<? extends Foo>");
    }
    Type responseType = getParameterUpperBound(0, (ParameterizedType) innerType);
    return new ResponseCallAdapter<>(responseType);
  }

View on GitHub (pinned to d0b112dad0)

Solutions

  1. Parameterize the return type with the body type: `CompletableFuture<User> getUser(@Path("id") long id);`
  2. For full HTTP responses: `CompletableFuture<Response<User>> getUser(@Path("id") long id);`
  3. Remove raw-types suppressions and resolve every compiler warning.

Example fix

// before
CompletableFuture getUser(@Path("id") long id);
// after
CompletableFuture<User> getUser(@Path("id") long id);
Defensive patterns

Strategy: validation

Validate before calling

import java.lang.reflect.*;
static void assertFuturesParameterized(Class<?> iface) {
  for (Method m : iface.getDeclaredMethods()) {
    Type rt = m.getGenericReturnType();
    if (rt.getTypeName().startsWith("java.util.concurrent.CompletableFuture")
        && !(rt instanceof ParameterizedType)) {
      throw new AssertionError("Raw CompletableFuture return type on " + m);
    }
  }
}

Try / catch

try {
  MyService svc = retrofit.create(MyService.class);
  CompletableFuture<User> f = svc.getUser(id);
} catch (IllegalStateException e) {
  throw new IllegalStateException("Service interface has a raw CompletableFuture return type: " + e.getMessage(), e);
}

Prevention

When it happens

Trigger: Declaring a method with a raw return type such as `CompletableFuture getUser();`. Thrown on first invocation of that method via the Retrofit proxy, inside Java8CallAdapterFactory.get() when `returnType` is not a ParameterizedType.

Common situations: Switching from `Call<T>` to CompletableFuture and omitting the generic; IDE inserting the raw type; using the java8 adapter on a project that previously returned `Call`.

Related errors


AI-assisted analysis of lysine-dev/retrofit@d0b112dad0 (2026-08-13). Data as JSON: /api/errors/b5b8911d7e58e02a. Report an issue: GitHub.