This page is also available in English.

Continue in English

Una gerarchia di errori, invece di throw new Error(...)

Pubblicato il 24 luglio 2026

Il modo più semplice per segnalare un fallimento in una libreria piccola è throw new Error('qualcosa è andato storto'). TinyDI ha invece una piccola gerarchia di classi: una base astratta ContainerError, e tre sottoclassi concrete — RegistrationError, ResolutionError, CircularDependencyError. La parte interessante di questa storia non è la gerarchia in sé, ma una decisione di design che la specifica lasciava intuire ma non esplicitava mai.

La gerarchia

ts
export abstract class ContainerError extends Error {
  protected constructor(
    message: string,
    public readonly token?: Token<unknown>,
  ) {
    super(message);
    this.name = new.target.name;
    Object.setPrototypeOf(this, new.target.prototype);
  }
}
 
export class RegistrationError extends ContainerError { /* ... */ }
export class ResolutionError extends ContainerError { /* ... */ }
export class CircularDependencyError extends ContainerError { /* ... */ }

ContainerError trasporta il Token coinvolto nel fallimento (quando esiste), così un gestore generico può registrare quale token ha causato il problema senza doversi preoccupare di quale sottoclasse concreta abbia intercettato:

ts
try {
  container.resolve(SomeToken);
} catch (error) {
  if (error instanceof ContainerError) {
    console.error(`DI failure for "${error.token?.description}": ${error.message}`);
  }
}

La decisione che la specifica lasciava implicita

La specifica del task nominava RegistrationError come parte della gerarchia attesa, ma non diceva mai esattamente quando dovesse scattare. La domanda di design ovvia: cosa succede se registri lo stesso token due volte? Esistono due risposte ragionevoli — lasciare che la seconda registrazione vinca silenziosamente ("vince l'ultima scrittura", l'opzione più permissiva), oppure lanciare un errore. TinyDI lancia un errore:

ts
private assertNotRegistered(token: Token<unknown>): void {
  if (this.registrations.has(token.symbol)) {
    throw new RegistrationError(
      token,
      `Token "${token.description}" is already registered. ` +
        'Call remove() first to replace it, or clear() to reset the container.',
    );
  }
}

È stata una scelta deliberata, non l'unica ragionevole: una classe di errore dichiarata dalla specifica ma non descritta richiedeva comunque una decisione di design per essere onorata come un contratto pubblico vero, invece di essere lasciata a quello che l'implementazione avrebbe fatto per prima.

La sovrascrittura silenziosa è una trappola in un container DI. Registrare per sbaglio lo stesso token due volte — una volta nel codice applicativo, una volta in un setup di test che ha dimenticato di ripulire, nel caso più comune — è esattamente il tipo di bug che diventa rumoroso e ovvio nel momento stesso in cui accade con un throw esplicito, e silenzioso e difficile da tracciare se la seconda registrazione vince e basta. Lanciare un errore costa un po' di comodità; lasciarlo correre costa una sessione di debug, settimane dopo, per capire perché viene risolta l'implementazione sbagliata.

La via di uscita

Poiché sovrascrivere lancia un errore, sostituire un'implementazione — più comunemente nei test — richiede un container.remove(token) esplicito (oppure container.clear()) prima di registrare di nuovo. Quel passaggio in più è il costo di questa decisione; significa anche che "volevo sostituire questo" è sempre visibile nel codice come una riga a sé, non implicito in una seconda chiamata di registrazione.

Cosa comporta per chi consuma la libreria

Una gerarchia di quattro classi al posto di un Error piatto è poco codice in più, ma permette a chi consuma la libreria di distinguere il tipo di fallimento quando serve (per esempio trattare un ResolutionError per un servizio opzionale in modo diverso da un RegistrationError emerso durante il bootstrap dell'app), pur potendo intercettare tutto ciò che riguarda la DI con un solo controllo instanceof ContainerError. CircularDependencyError, la quarta sottoclasse, ha un post tutto suo — lì la parte interessante non era la classe in sé ma il formato del messaggio. Vedi la API Reference per la superficie completa degli errori.

per navigare per selezionare Esc per chiudere