Angular Troubleshooting: 404 on Refresh, ExpressionChanged, EventTarget and SSL on Localhost

Angular Tutorial: Beginner to Advanced · 18. Quality, security and delivery · 6 min read · Updated

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 serve over 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:

  1. Use signals for state that more than one component changes. Angular schedules a new draw by itself.
  2. Move the change earlier: set the value in the constructor or ngOnInit of the component that owns it, not in an after* hook of a child.
  3. Let the data flow downwards: the parent owns the state and passes it with input(); the child asks for changes with output().
  4. Last resort: wrap the change in Promise.resolve().then(...) or setTimeout. 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 the imports of the component (or module) that uses it.
  • NullInjectorError: No provider for X: add provideHttpClient() or providedIn: 'root', or provide it in the test.
  • Can't bind to 'ngModel': import FormsModule.
  • 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 setTimeout everywhere.
  • Using $any($event.target).value to silence the type checker. It hides real bugs.
  • Setting useHash only because the server returns 404 on refresh.
  • Using a self-signed certificate on a production server.

Try it yourself

  1. Deploy a build to a static server without a fallback rule, refresh a deep link, then add the fix.
  2. Reproduce NG0100 in your own app and fix it with a signal.
  3. Run ng serve --ssl and look at the certificate warning.

Quick check

  1. Why does refresh give 404 but clicking a link does not?
  2. When does Angular check for NG0100?
  3. 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.

Frequently asked questions

Is NG0100 only a development error?

The check runs only in development, but the bug behind it can show a stale screen in production, so always fix it.

Why does nginx need try_files?

Because the server only knows real files. try_files falls back to index.html, which lets the Angular router handle the path.