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
- Parameterize Future with the body type: `Future<User> getUser()`.
- If you need headers/status, wrap as `Future<Response<User>>`.
- 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
- Always declare the full generic signature: Future<User>, never raw Future.
- From Java interop, never let type inference drop the generic argument.
- Keep an integration test that resolves every service method at build time.
- Treat raw-type warnings as errors in your IDE/compiler.
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
- Response must be parameterized as Response<Foo> or Response<
- Response must be parameterized as Response<Foo> or Response<
- Result must be parameterized as Result<Foo> or Result<? exte
- ListenableFuture return type must be parameterized as Listen
- ${name} return type must be parameterized as ${name}<Foo> or
AI-assisted analysis of square/retrofit@d0b112dad0 (2026-08-04).
Data as JSON: /data/errors/6179d4843f973539.json.
Report an issue: GitHub.