Fix NG8118: Required Input Signal in Angular Constructor
Resolve NG8118 error when accessing required input signals in Angular component constructors. Learn why it happens, Angular input lifecycle timing, and fixes like ngOnInit or computed signals for input required signals.
Why does accessing a required input signal in an Angular component constructor cause NG8118 error: ‘required input does not have a value’?
I have an Angular component using input.required<string>() accessed in the constructor:
export class ApplicationComponent {
protected key = input.required<string>();
constructor() {
const key = this.key();
// do other things with key
}
}
Running ng serve or ng build results in:
✘ [ERROR] NG8118: `key` is a required `input` and does not have a value in this context. [plugin angular-compiler]
Angular documentation states that required inputs always have a value and cannot have defaults. Using @angular/compiler@21.0.6. What causes this issue and how to fix it?
The NG8118 error hits when you try to read a required input signal like input.required<string>() in an Angular component’s constructor because Angular hasn’t bound the value yet—it’s a build-time check from the compiler. Angular input signals, especially required inputs, guarantee a value only after initialization, typically in ngOnInit or later lifecycle hooks. Quick fix? Move that access to ngOnInit() or wrap it in a computed() signal.
Contents
- Understanding the NG8118 Error
- Why Constructors Can’t Access Required Inputs
- Angular Component Lifecycle and Input Timing
- How input.required() Signals Work
- Common Pitfalls with Signal Inputs
- Step-by-Step Fixes for NG8118
- Best Practices for Angular Inputs
- Migrating from @Input to Signals
- Troubleshooting Edge Cases
Understanding the NG8118 Error
Ever built an Angular app and watched the compiler throw NG8118 right in your face? It screams: “key is a required input and does not have a value in this context.” This isn’t a runtime crash—it’s smarter. Angular’s compiler does static analysis on signal-based inputs to catch you reading a required input before it’s safe.
Picture this: your component declares protected key = input.required<string>();. Looks solid, right? But slap this.key() inside the constructor, and boom—build fails. Why? The docs spell it out clearly in the inputs guide. The compiler scans every read of that signal. If it spots one too early—like constructor time—it flags NG8118 because no parent component has passed a value yet.
And it’s picky about context. Templates? Fine. Effects or computeds? Good. Constructor? Hard no. This guardrail prevents subtle bugs where you’d assume a value exists but get undefined at runtime.
Why Constructors Can’t Access Required Inputs
Constructors in Angular components run before the framework touches anything Angular-specific. They’re plain TypeScript, fired off when the class instantiates. No inputs bound, no change detection, nada.
Think about it. Your code:
export class ApplicationComponent {
protected key = input.required<string>();
constructor() {
const key = this.key(); // NG8118 here
console.log(key.toUpperCase());
}
}
By constructor time, Angular hasn’t parsed the template or queried the parent for @Input equivalents. The signal sits empty. Even though input.required promises “always a value,” that’s post-construction. The error docs nail it: “A required input was accessed but no value was bound. This is commonly happening when the input is read as part of class construction.”
But wait—aren’t signals reactive magic? Sure, but initialization order matters. Constructor → input binding → lifecycle hooks. Mess that up, and the compiler (in your case, v21.0.6) enforces it at build time. No shipping broken code.
Community threads echo this too. Developers hit walls migrating to signals, wondering why old @Input() worked (spoiler: it was optional, so undefined was fair game).
Angular Component Lifecycle and Input Timing
Angular’s lifecycle hooks aren’t random—they follow a strict sequence, especially with signals. Here’s the kicker for Angular input signals.
| Hook | Inputs Available? | Notes |
|---|---|---|
| Constructor | ❌ No | Plain TS, no Angular magic yet |
| ngOnChanges | ✅ First change | Required inputs set here if bound |
| ngOnInit | ✅ Fully initialized | Safe spot for most logic |
| After hooks | ✅ Always | Effects, computeds, etc. |
From the lifecycle guide, “Inputs are guaranteed to be available in the ngOnInit lifecycle hook and afterwards.” Constructor? Zilch. Your ng serve or ng build enforces this via static checks.
Why the split? Performance and predictability. Signals update efficiently, but early reads could trigger unnecessary work or errors. In signal components, this timing gets even tighter—inputs wire up just before ngOnChanges.
Quick test: Yank that constructor code to ngOnInit(). Builds clean, runs smooth. Mystery solved?
How input.required() Signals Work
Diving deeper, input.required<T>() from @angular/core creates a read-only signal without defaults. The API reference says it plain: “Required inputs always have a value of the given input type and cannot have default values.”
Unlike optional input(), which starts with undefined or a default, required ones demand a bind from the parent template. Compiler verifies this—no loose ends.
Under the hood:
- Declare:
key = input.required<string>(); - Parent binds:
<app-component [key]="'value'"> - Angular sets signal post-construction.
Access it reactively: computed(() => this.key().toUpperCase()). That’s when magic happens—recomputes on changes.
But constructors ignore this flow. They’re for dependency injection only, not data access. Angulararchitects breaks it down nicely in their signal inputs post: “Reading it in the constructor happens before Angular has supplied a value.”
Got strict TypeScript? strictPropertyInitialization adds another layer, forcing init checks. Signals sidestep some, but not constructor reads.
Common Pitfalls with Signal Inputs
You’re not alone. GitHub issues like #53804 rant about constructor timing: “Why call the constructor before required inputs are ready?” It’s a design choice—keeps components lightweight.
Other traps:
- Static queries:
viewChild.required()suffers similarly. - Effects in fields:
effect(() => console.log(this.key()));—runs too early if not in a hook. - Migration pain:
@Input() key!: string;let you!away types, but signals demand discipline. - Nested components: Child reads parent’s input before bind propagates.
Stack Overflow classics confirm: Inputs never in constructors, always ngOnInit. ESLint even proposes rules against it.
And that “always have a value” doc bit? Contextual—after binding. No bind? Template errors elsewhere.
Step-by-Step Fixes for NG8118
Ready to squash it? Here’s your how-to.
Fix 1: Move to ngOnInit (Simplest)
export class ApplicationComponent implements OnInit {
protected key = input.required<string>();
ngOnInit() {
const key = this.key();
// Your logic here—safe!
}
}
Fix 2: Use computed for Derived State
protected uppercaseKey = computed(() => this.key().toUpperCase());
Template: {{ uppercaseKey() }}. Reactive, no hooks needed.
Fix 3: Effects for Side Effects
constructor() {
effect(() => {
const key = this.key();
console.log(key); // Runs after bind
});
}
Fix 4: Make Optional (Last Resort)
key = input<string>(); with default. Loses “required” guarantee.
Build again—NG8118 gone. Test with missing bind: Parent template errors catch it.
Best Practices for Angular Inputs
Don’t just fix—level up.
- Lifecycle discipline: Constructor for DI/services only. Inputs in
ngOnInit+. - Reactive first: Computeds/effects over manual reads.
- Type tight: Leverage compiler checks—embrace the pain.
- Parent responsibility: Always bind requireds:
[key]="someValue". - Docs habit: Check inputs guide for updates—Angular evolves fast.
Pro tip: Pair with OnPush change detection. Signals shine there.
For forms? model() inputs handle reactivity better.
Migrating from @Input to Signals
Old school @Input({required: true})? It warned at runtime. Signals shift to build-time.
Steps:
- Swap:
@Input() key!: string;→key = input.required<string>(); - Ditch
ngOnChangesfor effects/computeds. - Hunt constructor reads—grep ‘this.[a-z]+()’ .
- Update templates:
[key]stays same.
Angular-eslint discussions push constructor bans. Future-proof now.
Troubleshooting Edge Cases
Still stuck?
- Version check:
@angular/compiler@21.0.6—update if older. - AOT off?
ng serve --aotenforces strictly. - Nested signals: Chain carefully.
- Server-side: Hydration waits for inputs too.
- No error but undefined? Runtime bind missing.
Log lifecycle: console.log('ctor', this.key()); vs ngOnInit. See the diff.
GitHub threads like ESLint #1321 suggest linters. Stack Overflow reminds: Lifecycle 101.
Sources
- NG0950: Required input is accessed before a value is set — Official error explanation and lifecycle fixes: https://angular.dev/errors/NG0950
- Accepting data with input properties — Detailed NG8118 analysis, compiler checks, and safe access patterns: https://angular.dev/guide/components/inputs
- input • Angular — API docs on input.required signals and guarantees: https://angular.dev/api/core/input
- Lifecycle • Angular — Component hook order with input availability table: https://angular.dev/guide/components/lifecycle
- Component Communication with Signals — Practical pitfalls, code examples for required inputs: https://www.angulararchitects.io/en/blog/component-communication-with-signals-inputs-two-way-bindings-and-content-view-queries/
Conclusion
NG8118 is Angular’s way of saying “not yet”—required input signals demand post-construction access to stay predictable. Shift to ngOnInit, computeds, or effects, and your Angular input woes vanish. Embrace it: cleaner code, fewer runtime gremlins. Next build? Smooth sailing.