codejump

AcademyAngular

Services and Async State with Signals

A service is Angular's unit of shared, reusable logic — its composable — and signal() is how it holds async state a component can read reactively.

2 min readUpdated

3 exercises

Waiting at the end of this page.

A service is Angular's version of a composable

Where Vue reaches for a composable and React for a custom hook, Angular reaches for an injectable service — a class holding state and behavior that any component can share through DI rather than reimplementing:

typescript
@Injectable({ providedIn: 'root' })
export class StorageService {
  get<T>(key: string): T | null {
    const raw = localStorage.getItem(key);
    return raw ? (JSON.parse(raw) as T) : null;
  }

  set<T>(key: string, value: T): void {
    localStorage.setItem(key, JSON.stringify(value));
  }
}

A localStorage wrapper that "never serializes on the way in" is a .set() that forgot JSON.stringify and stored an object directly — which localStorage silently coerces to the string "[object Object]" instead of throwing — and the matching bug on the way out is a .get() that returns the raw string without JSON.parse, so callers receive text where they expected the typed value back.

signal() turns a promise's outcome into something a template can read directly

Async data has no natural place in a template until something bridges it into synchronous, reactive state — that bridge is a signal set from inside the .then/.catch of the request it represents:

typescript
user = signal<User | null>(null);
status = signal<'loading' | 'success' | 'error'>('loading');

async load(id: string): Promise<void> {
  try {
    this.user.set(await this.api.getUser(id));
    this.status.set('success');
  } catch {
    this.status.set('error');
  }
}

A loader "stuck forever on loading" usually means the success or error branch never actually calls .set() on the status signal — the request completes, the data even arrives, but nothing tells the template the state changed. The other half of that same drill: an error handler that assumes every rejection is an Error and reads .message off it will itself throw when something rejects with a plain string or object instead, which is exactly the kind of untyped catch block unknown exists to force you to guard.

What interviewers check

Whether you default to a service, backed by DI, for anything two components need to share, and whether an async signal's error path is something you actually tested — not just its happy path — since the loading state that never resolves is the bug every reviewer has seen shipped at least once.

Now practice it

1 quiz · 2 drills

Reading this page is the cheap half. These are the exercises that make you use it.