Every Angular developer meets the same few errors. This lesson explains why each one happens, so you can fix it and recognise its cousins.
What you will learn
- Why a refresh gives "404" or "Requested URL not found", and the server fix
ExpressionChangedAfterItHasBeenCheckedError(NG0100)Property 'value' does not exist on type 'EventTarget'- Running
ng serveover HTTPS on localhost
1. Refreshing a page gives 404
Symptom: /products/5 works when you click a link, but pressing refresh shows "404" or "Requested URL not found".
Why: with path URLs only the router in the browser knows /products/5. When you refresh, the browser asks the server for that path. The server has no such file and says 404.
Fix: tell the server to answer every unknown path with index.html. Angular then starts and the router reads the URL.
# nginx
location / {
try_files $uri $uri/ /index.html;
}
# Apache .htaccess
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ /index.html [L]
// Express (Node)
app.use(express.static('dist/my-app/browser'));
app.get('*splat', (req, res) => res.sendFile(path.join(__dirname, 'dist/my-app/browser/index.html')));
Firebase Hosting needs a rewrites rule to /index.html, Netlify a _redirects file with /* /index.html 200. If you cannot change the server, the last resort is withHashLocation().
If you deploy under a sub-folder, build with ng build --base-href /my-app/.
2. ExpressionChangedAfterItHasBeenCheckedError
Symptom (development only): NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'a'. Current value: 'b'.
Why: Angular draws the page top to bottom. In development it then checks again to be sure nothing changed. If a child changes a value that the parent already drew, the two passes disagree. Production does not throw, but the screen can be wrong (the test at the end of this lesson shows the stale text), so fix it.
Here is a broken example. A child writes to a shared object while the parent is still being drawn, after the parent has already printed the old value:
// src/app/trouble/changed-after-checked.ts
import { AfterViewInit, Component, Injectable, inject, signal } from '@angular/core';
// Shared state kept in a plain property: Angular cannot know when it changes.
@Injectable({ providedIn: 'root' })
export class PlainStatus {
text = 'waiting';
}
@Component({ selector: 'app-bad-child', template: '<p>child</p>' })
export class BadChild implements AfterViewInit {
private readonly status = inject(PlainStatus);
ngAfterViewInit() {
this.status.text = 'child is ready'; // changes the value after the parent printed it
}
}
@Component({
selector: 'app-bad-parent',
imports: [BadChild],
template: `
<p>Status: {{ status.text }}</p>
<app-bad-child />
`,
})
export class BadParent {
protected readonly status = inject(PlainStatus);
}
// Fixed version: the shared state is a signal, so Angular knows it changed and draws again.
@Injectable({ providedIn: 'root' })
export class SignalStatus {
readonly text = signal('waiting');
}
@Component({ selector: 'app-good-child', template: '<p>child</p>' })
export class GoodChild implements AfterViewInit {
private readonly status = inject(SignalStatus);
ngAfterViewInit() {
this.status.text.set('child is ready');
}
}
@Component({
selector: 'app-good-parent',
imports: [GoodChild],
template: `
<p>Status: {{ status.text() }}</p>
<app-good-child />
`,
})
export class GoodParent {
protected readonly status = inject(SignalStatus);
}
Read the broken parent in order: Angular prints Status: waiting, then draws the child, and only afterwards the child's ngAfterViewInit sets "child is ready". In the second check Angular sees a different value than it printed, and complains.
Ways to fix it, best first:
- Use signals for state that more than one component changes. Angular schedules a new draw by itself.
- Move the change earlier: set the value in the constructor or
ngOnInitof the component that owns it, not in anafter*hook of a child. - Let the data flow downwards: the parent owns the state and passes it with
input(); the child asks for changes withoutput(). - Last resort: wrap the change in
Promise.resolve().then(...)orsetTimeout. This hides design problems, so avoid it.
3. Property 'value' does not exist on type 'EventTarget'
Symptom: the template (input)="name = $event.target.value" fails to build.
Why: TypeScript only knows that $event.target is an EventTarget, which has no value. The strict template checker stops you, and rightly.
Fix: read the value from a template variable, or cast in the class.
// src/app/trouble/name-input.ts
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-name-input',
template: `
<!-- 1. simplest: template reference variable -->
<input #box (input)="name.set(box.value)" />
<!-- 2. call a method and cast there -->
<input (input)="onInput($event)" />
<p>Hello {{ name() }}</p>
`,
})
export class NameInput {
name = signal('');
onInput(event: Event) {
this.name.set((event.target as HTMLInputElement).value);
}
}
Or skip the problem with [(ngModel)] or a reactive FormControl.
4. HTTPS on localhost
Some browser features (service workers, geolocation, camera, secure cookies) need HTTPS even in development.
ng serve --ssl
Angular makes a self-signed certificate, and the browser warns that it is not trusted. To avoid the warning, create a local certificate with a tool such as mkcert, then:
ng serve --ssl --ssl-cert localhost.pem --ssl-key localhost-key.pem
Or save it in angular.json under serve.options as "ssl": true, "sslCert": "...", "sslKey": "...". Use this only for development, never for production, where your host provides a real certificate.
5. Other quick fixes
NG8001: 'app-x' is not a known element: add the component to theimportsof the component (or module) that uses it.NullInjectorError: No provider for X: addprovideHttpClient()orprovidedIn: 'root', or provide it in the test.Can't bind to 'ngModel': importFormsModule.- A page that shows only after a click: with zoneless apps make sure changed state is a signal.
Test it
// src/app/trouble/trouble.spec.ts
import { TestBed } from '@angular/core/testing';
import { BadParent, GoodParent, PlainStatus } from './changed-after-checked';
import { NameInput as Name } from './name-input';
describe('troubleshooting', () => {
it('a plain property changed by a child leaves the screen stale', () => {
const fixture = TestBed.createComponent(BadParent);
fixture.detectChanges();
// the value changed, but the parent printed the old one
expect(TestBed.inject(PlainStatus).text).toBe('child is ready');
expect(fixture.nativeElement.textContent).toContain('Status: waiting');
});
it('a signal makes Angular draw again, so the screen is correct', async () => {
const fixture = TestBed.createComponent(GoodParent);
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.nativeElement.textContent).toContain('child is ready');
});
it('reads the typed value without the EventTarget error', async () => {
const fixture = TestBed.createComponent(Name);
await fixture.whenStable();
const input = fixture.nativeElement.querySelectorAll('input')[1] as HTMLInputElement;
input.value = 'Asha';
input.dispatchEvent(new Event('input'));
await fixture.whenStable();
expect(fixture.nativeElement.textContent).toContain('Hello Asha');
});
});
Common mistakes
- Fixing NG0100 with
setTimeouteverywhere. - Using
$any($event.target).valueto silence the type checker. It hides real bugs. - Setting
useHashonly because the server returns 404 on refresh. - Using a self-signed certificate on a production server.
Try it yourself
- Deploy a build to a static server without a fallback rule, refresh a deep link, then add the fix.
- Reproduce NG0100 in your own app and fix it with a signal.
- Run
ng serve --ssland look at the certificate warning.
Quick check
- Why does refresh give 404 but clicking a link does not?
- When does Angular check for NG0100?
- What is the safest way to read an input's value?
Next, continue with Angular Module Loaders: SystemJS, Webpack and Today's esbuild and Vite Build.