Debugging NG0202 invalidFactoryDep in Storybook Production Builds
There is a specific kind of Angular bug that almost feels unfair.
Everything works locally. The application behaves exactly as expected.
Even in production, nothing seems wrong.
But then you open Storybook, and suddenly something is missing.
Not broken. Just… not there.
That was the situation here. A notification component stopped appearing in Storybook. The container element was present in the DOM, the layout looked correct, but the notification itself never rendered.
The only real hint was a runtime error:
NG0202: invalidFactoryDep
If you have ever seen that error, you know it does not exactly point you in a helpful direction.
If you see a component container in the DOM but the component itself never renders, and Angular throws NG0202, you are usually dealing with a dependency injection factory problem.
The first assumption: “This must be a Storybook issue”
The problem showed up while running Storybook with production settings. Outside Storybook, the application appeared to work fine. Locally, everything worked perfectly.
So the first assumption was obvious. This must be something Storybook-specific.
That assumption made sense.
And it was wrong.
Storybook did not introduce the bug. It simply exposed it.
Step one: reproduce the Storybook environment more realistically
The breakthrough came from treating Storybook the same way we treat production builds.
Not just running the default dev setup, but running Storybook with production-like compilation, bundling, and AOT enabled.
That made the behavior consistent.
To remove guesswork, Storybook was run with production configuration:
nx storybook frontend-library -c production
And finally, as a static build:
nx build-storybook frontend-library -c production
npx serve dist/storybook/frontend-library
At that point the issue became fully reproducible. The notification container appeared, but the notification component itself did not.
That confirmed this was not just a random local setup problem.
What was actually going wrong
The failing piece was a custom notification component that extended a base notification class from a third-party library.
That base class expects several dependencies in its constructor. Among them are a service instance, a data object describing the notification, and Angular’s NgZone.
In development mode, Angular managed to resolve those dependencies implicitly through inheritance.
In a stricter build setup, Angular tried to generate a factory for the subclass and failed to resolve one of the inherited constructor dependencies.
When that happens, Angular cannot create the component at all.
The container element still exists, which makes the UI look almost correct, but the component itself never renders.
That explains the strange “half-working” state.
Why NgZone suddenly mattered
Notification components are not passive UI elements. They rely on timers, animations, overlays and change detection.
All of that depends on Angular’s zone system.
In the full application bootstrap, NgZone was always present in the expected way. In Storybook and in the production-like setup, Angular could no longer reliably infer that dependency through inheritance alone.
Once dependency resolution failed, Angular simply stopped creating the component.
The fix: stop letting Angular guess
Instead of relying on Angular to infer what the base class constructor needed, the dependencies were injected explicitly and passed to the base class manually.
public override readonly notificationData: NotificationData;
protected override readonly notificationService: NotificationService;
constructor() {
const notificationService = inject(NotificationService);
const notificationData = inject(NotificationData);
const ngZone = inject(NgZone);
super(notificationService, notificationData, ngZone);
this.notificationService = notificationService;
this.notificationData = notificationData;
}
Nothing clever is happening here.
The code simply states what the base class already required. The behavior does not change.
Angular cannot always infer constructor dependencies through inheritance in AOT builds, so passing them explicitly makes the factory deterministic.
Was this really a production code change for a Storybook fix?
We did have a moment of doubt here.
Because the application itself was fine. Locally everything worked, and even in production we did not see any visible issues.
The failure only showed up in Storybook, especially when running it in a production-like configuration.
So the question was fair: do we really want to change production code just to fix a Storybook story?
What made the decision easier is this: the change is not Storybook-specific.
We did not add a workaround or a conditional path. We simply made the dependencies that the base class already needs explicit.
In development mode Angular managed to infer those dependencies through inheritance. In a stricter build setup, that inference became unreliable.
Storybook revealed it first, but it was not the root cause.
So we treated this as a stability improvement. It makes the code more deterministic across environments, and it removes a “works by accident” dependency on dev mode behavior.
“But why did it work before?”
Because development mode is forgiving.
JIT compilation, looser metadata handling and fewer optimizations allow Angular to resolve things that break once AOT and tree-shaking are involved.
Storybook with production settings removes that flexibility. It forces Angular to be precise.
When something is ambiguous, it breaks.
In this case, that strictness exposed a weakness that had been there all along.
Does this risk breaking existing notifications?
This question came up immediately, and it is a fair one.
The important thing to understand is that these dependencies were already required by the base class.
This change does not introduce new behavior or new requirements. It simply makes those requirements explicit.
If there were a place where these dependencies were missing, that notification component would already have been broken.
This change just makes the failure deterministic instead of accidental.
A debugging pattern worth remembering
When you run into an Angular issue that works locally but fails in Storybook, pause before changing code.
Try to reproduce the strictest environment you have. Run Storybook with production configuration. Build it statically. Remove the dev assumptions.
Storybook did not break production. It simply ran Angular in a stricter mode that revealed a hidden DI edge case.
Once you do that, problems like this stop being abstract. They become normal debugging again.
And that is the real lesson here.
Frameworks at Crossroads Europalaan 93, 3526 KP Utrecht