grpc/grpc-java · error · IllegalStateException

ScheduledExecutorService not set in Builder

Error message

ScheduledExecutorService not set in Builder

What it means

NameResolver.Args.getScheduledExecutorService throws IllegalStateException when no ScheduledExecutorService was provided to the Args.Builder before this getter is called. The field is optional at build time but required at read time, so the accessor enforces the invariant. It signals that the caller (typically a NameResolver implementation or a NameResolverProvider wiring path) expects an executor the builder never set.

Source

Thrown at api/src/main/java/io/grpc/NameResolver.java:425

    public SynchronizationContext getSynchronizationContext() {
      return syncContext;
    }

    /**
     * Returns a {@link ScheduledExecutorService} for scheduling delayed tasks.
     *
     * <p>This service is a shared resource and is only meant for quick tasks. DO NOT block or run
     * time-consuming tasks.
     *
     * <p>The returned service doesn't support {@link ScheduledExecutorService#shutdown shutdown()}
     *  and {@link ScheduledExecutorService#shutdownNow shutdownNow()}. They will throw if called.
     *
     * @since 1.26.0
     */
    @ExperimentalApi("https://github.com/grpc/grpc-java/issues/6454")
    public ScheduledExecutorService getScheduledExecutorService() {
      if (scheduledExecutorService == null) {
        throw new IllegalStateException("ScheduledExecutorService not set in Builder");
      }
      return scheduledExecutorService;
    }

    /**
     * Returns the {@link ServiceConfigParser}.
     *
     * @since 1.21.0
     */
    public ServiceConfigParser getServiceConfigParser() {
      return serviceConfigParser;
    }

    /**
     * Returns the value of a custom arg named 'key', or {@code null} if it's not set.
     *
     * <p>While ordinary {@link Args} should be universally useful and meaningful, custom arguments
     * can apply just to resolvers of a certain URI scheme, just to resolvers producing a particular

View on GitHub (pinned to 64daddc1f3)

Solutions

  1. Call .scheduledExecutorService(executor) on the Args.Builder before building — e.g. pass the channel's or a shared ScheduledThreadPoolExecutor.
  2. When implementing a NameResolver, avoid depending on the executor if args may lack it, or check availability first and provide a default executor.
  3. If you control provider code, update it to copy all fields from existing Args (including scheduledExecutorService) when rebuilding/wrapping args.
  4. Verify grpc-java version alignment: NameResolverProvider signatures changed in 1.26+; rebuild custom providers against the current API.

Example fix

// before
NameResolver.Args args = NameResolver.Args.newBuilder()
    .setDefaultPort(443)
    .setServiceConfigParser(parser)
    .build();
// after
NameResolver.Args args = NameResolver.Args.newBuilder()
    .setDefaultPort(443)
    .setServiceConfigParser(parser)
    .setScheduledExecutorService(sharedScheduler)
    .build();
Defensive patterns

Strategy: try-catch

Validate before calling

NameResolver.Args.Builder b = NameResolver.Args.newBuilder()...; if (b != null) { /* ensure setScheduledExecutorService was called before build() */ }

Type guard

boolean hasScheduler(NameResolver.Args args) { try { return args.getScheduledExecutorService() != null; } catch (IllegalStateException e) { return false; } }

Try / catch

ScheduledExecutorService ses; try { ses = args.getScheduledExecutorService(); } catch (IllegalStateException e) { ses = Executors.newSingleThreadScheduledExecutor(r -> { Thread t = new Thread(r, "name-resolver"); t.setDaemon(true); return t; }); }

Prevention

When it happens

Trigger: Calling getScheduledExecutorService() on NameResolver.Args built without builder.scheduledExecutorService(...). Happens in custom NameResolver/NameResolverProvider implementations that read the executor, or when older provider/wrapper code (wrap, newNameResolver) builds Args without the executor.

Common situations: Upgrading grpc-java: NameResolverProvider APIs changed and custom providers construct Args without setting the executor; copying a NameResolver.Args builder but omitting scheduledExecutorService; a library's NameResolver assuming the executor exists on older args instances.

Understand the failure class

Background: "is required", "must be set", "missing required field": configuration validation errors across open-source libraries — this error's family across 36 libraries.

Related errors


AI-assisted analysis of grpc/grpc-java@64daddc1f3 (2026-09-08). Data as JSON: /api/errors/a8c09f7e853c403f. Report an issue: GitHub.