skip to content

What is ServiceConfigurationError, when is it thrown, and what are the common operational pitfalls when wiring SPI across the classpath and module path?

level: seniorimportance: should knowfreq 35%

answer

  1. ServiceConfigurationError is an Error, thrown at iteration/get(), not load()
  2. Causes: missing class, not a subtype, no no-arg ctor, ctor throws
  3. Module-path #1 bug: missing `uses`
  4. Classpath bug: stale entry or unmerged META-INF/services in shaded jars
  5. Catch per-provider via stream() to stay resilient

basics

~20 s

ServiceConfigurationError is thrown when a declared provider can't be loaded or instantiated — bad class name, no public no-arg constructor, or it doesn't implement the service. It surfaces while you iterate the ServiceLoader, not when you call load(). Typical pitfalls: forgetting uses on the module path, stale META-INF/services entries, and missing constructors.

solid answer

~50 s

ServiceConfigurationError is an Error (not a checked Exception) thrown by ServiceLoader when something declared as a provider can't actually be used: the class named in META-INF/services or provides...with doesn't exist, isn't a subtype of the service, has no accessible public no-arg constructor (or provider() method), or its construction throws. Because discovery and instantiation are separate, this error appears during iteration or Provider.get(), not at load() time — so you must guard the loop, not just the load call. Operational pitfalls: forgetting `uses` in the consumer module (silent empty results on the module path); a stale or typo'd META-INF/services line after refactoring; relying on provider order; a provider on neither the class nor module path; and mixing automatic modules with explicit ones so discovery behaves differently than expected. To be resilient, iterate via stream() and catch ServiceConfigurationError per provider so one broken provider doesn't kill the rest.

go deeper

for a junior

Knows that a bad provider causes an error when loading services and that the provider needs a no-arg constructor.

for a middle

Lists the causes of ServiceConfigurationError and knows it appears during iteration; aware of the missing-uses pitfall.

for a senior

Distinguishes classpath vs module-path failure modes (shaded-jar merging, automatic modules, uses), writes per-provider resilient loading.

for a principal

Owns SPI deployment robustness: packaging/merge strategy, graceful degradation policy, observability for provider failures, and migration across class/module path.

## What `ServiceConfigurationError` is `java.util.ServiceConfigurationError` is a subclass of **`Error`** (not `Exception`), thrown by `ServiceLoader` when a **declared** provider cannot be turned into a usable instance. It being an `Error` signals "a configuration/deployment problem," but in practice you often *do* want to catch it to degrade gracefully. ## Exactly when it is thrown The crucial timing fact: **discovery and instantiation are separate phases.** `ServiceLoader.load(...)` performs discovery and **never** throws `ServiceConfigurationError`. The error is raised **later**, when the loader actually tries to load/construct a specific provider — i.e. **during iteration** (`for`/`Iterator.next()`) or when you call **`Provider.get()`** on a stream element. Common triggers: - The class named in the provider declaration **does not exist** (typo, renamed/removed during refactor, wrong package). - The named class **is not a subtype** of the service interface. - The class has **no accessible public no-argument constructor** and **no public static `provider()` method**. - The constructor or `provider()` method **throws** an exception. - (Module path) The provider class **cannot be instantiated** by the module system due to access rules — usually a sign of a misdeclared `provides`. ## The classpath vs module-path matrix of pitfalls **Classpath (`META-INF/services`):** - **Stale / typo'd entry.** The file is plain text and untyped; a rename leaves a dangling class name that only fails at iteration. Keep these files in sync with refactors. - **Duplicate jars / shading.** Two copies of a provider jar produce duplicate or conflicting providers; fat-jar/shade plugins that don't *merge* `META-INF/services` files will silently drop providers — you must configure a ServicesResourceTransformer or equivalent. - **Wrong file path / name.** The file must be named exactly the fully-qualified service interface name, one impl class per line. **Module path (`module-info.java`):** - **Forgetting `uses` in the consumer** → `ServiceLoader.load` returns **nothing**, no error, no warning. This is the single most common JPMS SPI bug. - **Exporting the impl package unnecessarily** (leaks it) — or, conversely, expecting plain `requires` to make a service visible (it doesn't; you need `uses`). - **Automatic modules.** A non-modular jar on the module path becomes an *automatic module*; its `META-INF/services` files are still honoured, but its name is derived and behaviour can surprise you when mixing with explicit modules. **Both:** - **Relying on order.** Provider order is unspecified; encode priority explicitly. - **Missing constructor / wrong visibility** — applies to either mechanism. - **Provider on neither path** — the service simply yields no providers. ## Writing resilient consumer code Because one bad provider can abort a classic `for` loop, prefer `stream()` and catch per element so the rest still load: ```java List<Codec> codecs = ServiceLoader.load(Codec.class).stream() .flatMap(p -> { try { return Stream.of(p.get()); } catch (ServiceConfigurationError e) { log.warn("Skipping broken Codec provider {}", p.type(), e); return Stream.empty(); } }) .toList(); ``` With the classic iterator you would wrap `it.hasNext()/it.next()` in try/catch, which is clumsier — another reason the stream API is preferred for robust loading. ## Debugging checklist 1. Is the provider actually on the active class/module path? 2. Module path: does the **consumer** declare `uses`, and the provider declare `provides ... with`? 3. Does the impl have a public no-arg constructor (or `provider()` method) and truly implement the service? 4. Classpath: is the `META-INF/services/<service>` file present, correctly named, with the right class names, and **merged** in any shaded jar? 5. Did you `reload()` after changing the path at runtime?

  • Your fat jar runs but ServiceLoader finds no providers, though they worked in dev. What's the likely cause?
    The shade/assembly plugin overwrote rather than merged the META-INF/services files, so only one (or none) survived. Configure a services resource transformer to concatenate them. On the module path, the analogue is a missing `uses` directive.
  • Should you catch ServiceConfigurationError even though it's an Error?
    Often yes. To keep one misconfigured provider from breaking the whole feature, iterate via stream() and catch ServiceConfigurationError around each get(), logging and skipping the bad one. Letting it propagate is fine only if any provider failure should be fatal.

saying these in an interview costs you the question

  • Expecting ServiceConfigurationError at load() time
  • Treating it as a checked Exception you must catch (it's an Error)
  • Forgetting that a missing `uses` produces silent empty results, not an error
  • Assuming a shade/fat-jar automatically merges META-INF/services files
  • Letting one broken provider abort the whole iteration without guarding

context