square/retrofit · error · IllegalStateException

Future return type must be parameterized as Future<Foo> or F

Error message

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

What it means

Thrown by the Scala call adapter factory when a service method returns the raw scala.concurrent.Future type without a type argument. The factory (ScalaCallAdapterFactory.java:64-67) requires a ParameterizedType so it can resolve the inner body/Response type and build the correct adapter; a raw Future has no body type, so it fails fast with IllegalStateException when the method is first invoked.

Source

Thrown at retrofit-adapters/scala/src/main/java/retrofit2/adapter/scala/ScalaCallAdapterFactory.java:65

 *   <li>Response wrapped body (e.g., {@code Future<Response<User>>}) returns a {@link Response}
 *       object for all HTTP responses and sets {@link IOException} for network errors
 * </ul>
 */
public final class ScalaCallAdapterFactory extends CallAdapter.Factory {
  public static ScalaCallAdapterFactory create() {
    return new ScalaCallAdapterFactory();
  }

  private ScalaCallAdapterFactory() {}

  @Override
  public @Nullable CallAdapter<?, ?> get(
      Type returnType, Annotation[] annotations, Retrofit retrofit) {
    if (getRawType(returnType) != Future.class) {
      return null;
    }
    if (!(returnType instanceof ParameterizedType)) {
      throw new IllegalStateException(
          "Future return type must be parameterized as Future<Foo> or Future<? 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);
    }

    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 Future with the body type: `Future<User> getUser()`.
  2. If you need headers/status, wrap as `Future<Response<User>>`.
  3. Avoid using raw Future from Java interop; declare the full generic signature explicitly.

Example fix

// before
@GET("users/{id}")
Future getUser(@Path("id") String id);

// after
@GET("users/{id}")
Future<User> getUser(@Path("id") String id);
Defensive patterns

Strategy: validation

Validate before calling

// Startup smoke test asserting scala Future is never used raw.
@Test void futureReturnTypesAreParameterized() {
  for (java.lang.reflect.Method m : MyService.class.getDeclaredMethods()) {
    if (m.isDefault()) continue;
    Type t = m.getGenericReturnType();
    assertFalse(
        t == scala.concurrent.Future.class,
        m.getName() + " returns raw Future; use Future<BodyType>");
  }
}

Try / catch

try {
  MyService service = retrofit.create(MyService.class);
  // force method resolution via a startup smoke test
} catch (IllegalStateException e) {
  throw new IllegalStateException(
      "Retrofit service method return type is invalid: " + e.getMessage(), e);
}

Prevention

When it happens

Trigger: A method declared as `@GET("x") Future getUser();` (raw Future). The check fires at line 64 when getRawType(returnType) == Future.class but returnType is not a ParameterizedType.

Common situations: Java/Scala interop where the generic was dropped; copy-paste from a sample that omitted the type parameter; IDE raw-type import of scala.concurrent.Future; migrating a method from Call<T> to Future and forgetting the argument.

Related errors


AI-assisted analysis of square/retrofit@d0b112dad0 (2026-08-04). Data as JSON: /data/errors/6179d4843f973539.json. Report an issue: GitHub.