When It Breaks: Reading Guice Stack Traces Without Crying
Series: Guice for JVM Engineers · I (Fundamentals) — Part 8 of 8
You will, eventually, mistype a binding. You’ll forget to bind an interface, ask for a type Guice can’t construct, or wire two classes into a hug they can’t escape. The good news — and it really is good news — is that Guice fails loudly and early, and its error messages are some of the most readable in the JVM ecosystem. The bad news is that the first time you see a wall of CreationException text it looks like a punishment. It isn’t. It’s a map. This post teaches you to read it.
Everything goes wrong at once (on purpose)
Here’s the single most important fact about Guice failures: the injector validates the entire graph when you create it, and reports every problem it finds at once. It does not stop at the first error and make you fix-rerun-fix-rerun. Guice.createInjector(...) throws a CreationException whose message lists all the things wrong with your configuration.
That’s deliberate, and it’s the payoff for Guice’s whole “explicit bindings, no classpath scanning” philosophy from Part 1 — because the wiring is known up front, it can all be checked up front. The CreationException Javadoc says it plainly: clients should catch it, log it, and stop execution. Your app doesn’t limp along half-wired into production; it refuses to start.
A CreationException reads like this:
com.google.inject.CreationException: Unable to create injector, see the following errors:
1) [Guice/MissingImplementation]: No implementation for GreetingRepository was bound.
Requested by:
1 : GreetingService.<init>(GreetingService.java:12)
\_ for 1st parameter
at AppModule.configure(AppModule.java:8)
Learn more:
https://github.com/google/guice/wiki/MISSING_IMPLEMENTATION
1 error
======================
Full classpath is ...
Read it top-down. “Unable to create injector, see the following errors” is the CreationException’s own headline. “1)” is the first numbered error of however many there are — get used to scanning for 2), 3), the count is the workload. [Guice/MissingImplementation] is the error tag, and that Learn more URL is real and worth clicking. Everything under “Requested by:” is the dependency chain that led Guice to need this type — read it as “who wanted this, and who wanted them.” That chain is the difference between “some binding is missing somewhere” and “the missing binding is the GreetingRepository that GreetingService’s constructor asked for."
"No implementation for X was bound”
This is the error you’ll hit most. The message is verbatim from Guice’s source — "No implementation for %s was bound." — and it means exactly what it says: something in the graph needs an X, and no module told Guice how to make one, and Guice couldn’t figure it out on its own (no @Inject constructor to fall back on, the way just-in-time bindings work from Part 3).
Ninety percent of the time the cause is one of:
- you bound the implementation but asked for the interface, and forgot the
bind(Interface).to(Impl)line; - the type needs a qualifier (
@Named, a binding annotation from Part 5) and you bound the unqualified version, or vice versa; - you forgot to install the module that owns the binding (Part 7).
The fix for the trace above is the missing link in the module:
class AppModule : AbstractModule() {
override fun configure() {
// GreetingService asked for a GreetingRepository; tell Guice which one.
bind(GreetingRepository::class.java).to(SqlGreetingRepository::class.java)
}
}class AppModule extends AbstractModule {
@Override protected void configure() {
// GreetingService asked for a GreetingRepository; tell Guice which one.
bind(GreetingRepository.class).to(SqlGreetingRepository.class);
}
}When the message instead names a qualifier — No implementation for String annotated with @Named("dbUrl") was bound — your bug is the annotation, not the type. Don’t go binding String; go bind @Named("dbUrl") to a value.
Circular dependencies
Now the spicy one. A needs B, B needs A. Each constructor wants the other fully built, and “fully built” is a state neither can reach first. With constructor injection, Guice can’t satisfy this — there’s no valid order to instantiate them — and you get a circular-dependency error.
class Chicken @Inject constructor(private val egg: Egg)
class Egg @Inject constructor(private val chicken: Chicken)
// Guice can't build either one first. Boom.class Chicken {
@Inject Chicken(Egg egg) { /* ... */ }
}
class Egg {
@Inject Egg(Chicken chicken) { /* ... */ }
}
// Guice can't build either one first. Boom.The honest fix is to rethink the design — a cycle is almost always two classes that secretly want to be three, with the shared concern extracted into a collaborator both depend on. Do that when you can. But when you genuinely need the cycle (an observer that calls back into its subject is the classic case), Guice gives you a clean tool: inject a Provider<T> instead of T. A Provider is a lazy handle — Guice hands it over immediately without building the target, and you call .get() later, after both objects exist.
import jakarta.inject.Inject
import jakarta.inject.Provider
class Chicken @Inject constructor(private val egg: Egg)
class Egg @Inject constructor(private val chickenProvider: Provider<Chicken>) {
// The Chicken isn't built when Egg is constructed — only when we ask.
fun hatch(): Chicken = chickenProvider.get()
}import jakarta.inject.Inject;
import jakarta.inject.Provider;
class Chicken {
@Inject Chicken(Egg egg) { /* ... */ }
}
class Egg {
private final Provider<Chicken> chickenProvider;
@Inject Egg(Provider<Chicken> chickenProvider) {
this.chickenProvider = chickenProvider;
}
// The Chicken isn't built when Egg is constructed — only when we ask.
Chicken hatch() { return chickenProvider.get(); }
}Breaking the cycle with a Provider (the jakarta.inject.Provider we met in Part 4) turns a hard ordering problem into a deferred lookup, and the construction graph becomes a tree again. Note that Guice can also paper over some cycles with proxies, but only when the injected type is an interface — for concrete classes it can’t, and you’ll see Tried proxying SomeType to support a circular dependency, but it is not an interface. That message is a nudge: use a Provider, or extract an interface, or fix the design. The proxy is a crutch, not a cure.
ProvisionException: the failures that wait
CreationException is a configuration failure — the graph itself is broken, caught at startup. ProvisionException is its runtime cousin: the wiring was valid, but actually building an instance blew up. Its Javadoc is one line — “Indicates that there was a runtime failure while providing an instance.” A @Provides method that threw, a constructor that hit a null config, a third-party Provider that timed out connecting to a database — those surface as a ProvisionException from the getInstance(...) or Provider.get() call, not at injector creation.
The practical lesson: a clean createInjector doesn’t mean every object constructs cleanly, only that every dependency can be satisfied. Side effects in constructors and @Provides methods are exactly the things that escape startup validation and bite you later. Keep construction boring.
Failing fast, harder
Guice already fails fast on missing bindings. You can crank it tighter:
Stage.PRODUCTION— pass it tocreateInjectorand Guice eagerly constructs all singletons at startup instead of lazily on first use. A@Provides @Singletonthat throws will then blow up now, at boot, instead of on the first request in production. (Stage.DEVELOPMENT, the default for plaincreateInjector, constructs singletons lazily for faster startup.)binder().requireExplicitBindings()— switches off just-in-time bindings entirely. Every type the injector provides must be explicitly bound in a module; nothing gets auto-constructed off an@Injectconstructor it happened to find. More ceremony, zero surprises about where an instance came from.binder().requireAtInjectOnConstructors()— forbids Guice from using a no-arg constructor it wasn’t told it could use. Classes must declare an@Injectconstructor to be injectable, making injectability an explicit, opt-in decision rather than an accident of having a default constructor.
class StrictModule : AbstractModule() {
override fun configure() {
binder().requireExplicitBindings()
binder().requireAtInjectOnConstructors()
// ... your bind(...) lines
}
}
val injector = Guice.createInjector(Stage.PRODUCTION, StrictModule())class StrictModule extends AbstractModule {
@Override protected void configure() {
binder().requireExplicitBindings();
binder().requireAtInjectOnConstructors();
// ... your bind(...) lines
}
}
Injector injector = Guice.createInjector(Stage.PRODUCTION, new StrictModule());For a long-lived service these three together are a sane default: explicit graph, no accidental injectability, no lazy time bombs.
Seeing the graph: the Grapher
When the graph gets big enough that you can’t hold it in your head, stop reading bindings and look at them. The guice-grapher extension (com.google.inject.grapher) walks an injector and emits a Graphviz DOT file you render to an image. The canonical usage, straight from the extension’s own demo:
import com.google.inject.Guice
import com.google.inject.grapher.graphviz.GraphvizGrapher
import com.google.inject.grapher.graphviz.GraphvizModule
import java.io.PrintWriter
fun graph(target: com.google.inject.Injector, path: String) {
PrintWriter(path, "UTF-8").use { out ->
val grapher = Guice.createInjector(GraphvizModule())
.getInstance(GraphvizGrapher::class.java)
grapher.setOut(out)
grapher.setRankdir("TB")
grapher.graph(target) // the injector you want to visualize
}
}import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.grapher.graphviz.GraphvizGrapher;
import com.google.inject.grapher.graphviz.GraphvizModule;
import java.io.PrintWriter;
void graph(Injector target, String path) throws Exception {
try (PrintWriter out = new PrintWriter(path, "UTF-8")) {
GraphvizGrapher grapher = Guice.createInjector(new GraphvizModule())
.getInstance(GraphvizGrapher.class);
grapher.setOut(out);
grapher.setRankdir("TB");
grapher.graph(target); // the injector you want to visualize
}
}Then dot -Tpng graph.dot -o graph.png and you’ve got a picture of your object graph — nodes are types, edges are dependencies. It’s the fastest way to answer “wait, why is this getting constructed?” for a graph too tangled to trace by eye, and it’s genuinely useful in a code review when someone swears their module is simple.
Gotchas
- The error count is the to-do list. When a
CreationExceptionreports “5 errors,” skim all five before fixing one — they’re often the same root mistake (a module you forgot to install) showing up five times, and fixing it clears the lot. - “Requested by:” is the chain that matters. Don’t fixate on the missing type; read who asked for it. The bug is usually a few frames up, in the constructor that declared a dependency you never bound.
- Cycles caught at startup are a gift. A circular dependency error at injector creation beats discovering the cycle as a
StackOverflowErrorat 3am. Treat it as design feedback, not an obstacle to route around with aProviderreflexively. CreationException≠ProvisionException. Startup-config failure versus runtime-construction failure. If your injector creates fine butgetInstancethrows, stop staring at your bindings — the bug is inside a constructor or@Providesmethod.- A green
createInjectorisn’t a green app. It proves the graph is satisfiable, not that every object builds. UseStage.PRODUCTIONto drag singleton construction failures into startup where you’ll actually see them. - The
Learn moreURL is not decoration. Each error tag links to a Guice wiki page for that exact failure mode. When a message is unfamiliar, click it before you start guessing.
What’s next
That’s Guice I (Fundamentals) — from “why hand-wiring hurts” all the way to reading a CreationException without flinching. You can now bind types every way Guice offers, supply construction logic with providers, disambiguate with qualifiers, manage lifecycles with scopes, organize the whole thing into composable modules, and diagnose it when it breaks. That’s a real, production-shaped command of the framework.
Now we go advanced. Guice II picks up where the fundamentals leave off, and it opens with the feature you reach for the moment one binding per type stops being enough: Part II, Part 1 — Multibindings, where you bind many implementations of a type into a Set or Map and let Guice collect them for you. See you in the advanced series.
Target keyword(s): guice CreationException, guice circular dependency.
Comments