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
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:
@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:
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 drillsReading this page is the cheap half. These are the exercises that make you use it.
- A Service as a Reusable "Composable"ProFix a localStorage wrapper service that never serializes going in and never parses coming back out.DrillSolve · MediumDrillSolve · Medium
- Reacting to Async Data with a SignalProFix a user loader stuck forever on "loading", and an error handler that assumes every rejection is an Error.DrillSolve · MediumDrillSolve · Medium
- RxJS & Observables · Operators & SubjectsOperator and Subject questions for the async patterns a signal-backed service eventually grows into.QuizTake · MediumQuizTake · Medium
Read next
Lifecycle Hooks and Custom Directives
ngOnInit is where setup belongs, ngOnDestroy where it gets torn down; a custom attribute directive follows the same discipline for its own listeners.
2 min
Derived State and Strongly-Typed Inputs
computed() replaces a hand-managed boolean that tracks the same thing state already implies; a discriminated-union @Input() lets the compiler enforce every branch.
2 min