TypeScript usa try, catch, finally e throw di JavaScript. La parte specifica di TypeScript è la variabile del catch: con strict attivo ha tipo unknown, quindi controlli cosa è stato lanciato prima di usarlo.
Il lato runtime (come si srotola lo stack, quali tipi di errore predefiniti esistono) è trattato in try/catch in JavaScript.
Perché la variabile del catch è unknown
JavaScript può lanciare qualsiasi cosa: un Error, una stringa, un numero, undefined, un oggetto di una libreria. TypeScript non può sapere quale, quindi con strict (il flag useUnknownInCatchVariables) la variabile è unknown e devi restringerla prima di usarla:
index.ts(5,34): error TS18046: 'e' is of type 'unknown'.
Senza strict, e è any e lo stesso codice compila, poi va storto a runtime quando viene lanciata una stringa: e.message è undefined, e qualsiasi cosa più profonda, come e.message.length, lancia un TypeError. Non puoi nemmeno cavartela con un'annotazione: catch (e: Error) è l'errore TS1196 (Catch clause variable type annotation must be 'any' or 'unknown' if specified).
Restringere l'errore
instanceof Error copre tutto ciò che è costruito su Error, compresi TypeError, SyntaxError, RangeError e le tue sottoclassi. Per tutto il resto, ripiega sulla conversione del valore in stringa. Un piccolo helper tiene brevi i punti di chiamata:
Un catch che gestisce solo alcuni errori deve rilanciare gli altri: if (!(e instanceof ValidationError)) throw e;. Ingoiare gli errori sconosciuti nasconde bug reali.
Lanciare errori
throw accetta qualsiasi espressione, e TypeScript non lo limita. Lancia comunque oggetti Error: portano con sé uno stack trace, e ogni controllo instanceof Error del codebase dipende da questo.
Una funzione che lancia sempre un errore ha tipo di ritorno never, che TypeScript usa per restringere dopo la chiamata:
Da ES2022, Error accetta un secondo argomento con una cause, che collega un errore di basso livello a quello che lanci: throw new Error("could not load settings", { cause: e }). Chi chiama può leggere err.cause (tipizzato unknown).
Classi di errore personalizzate
Una sottoclasse di Error permette a chi chiama di distinguere i fallimenti con instanceof e di portare dati extra. Imposta name, perché quello ereditato è "Error" e compare nei log e in String(err):
Controlla per prima la classe più specifica, perché un NotFoundError è anche un HttpError. Il modificatore override qui è facoltativo, a meno che noImplicitOverride sia attivo.
Le guide più vecchie aggiungono Object.setPrototypeOf(this, new.target.prototype) a ogni costruttore di errore. Serviva quando le classi venivano compilate in funzioni ES5, dove instanceof si rompeva per le sottoclassi di Error. TypeScript 7 non supporta più target: "es5" (segnala TS5108: Option 'target=ES5' has been removed), e con ES2015 o successivi il nativo class ... extends Error funziona, come sopra.
Il pattern Result
TypeScript non tiene traccia degli errori che una funzione può lanciare, quindi nulla ricorda a chi chiama di gestirli. Per i fallimenti previsti (input non valido, un record mancante), restituire un valore che dice "successo o fallimento" porta l'errore dentro il sistema di tipi:
Leggere r.value prima di controllare r.ok è un errore di compilazione, ed è proprio questo lo scopo. Usa le eccezioni per l'imprevisto (bug, una connessione caduta) e i valori Result per gli esiti su cui chi chiama deve decidere.
Errori nel codice asincrono
Una promise rifiutata diventa un errore lanciato nel punto dell'await, quindi try/catch attorno ad await funziona allo stesso modo, e la variabile è di nuovo unknown. L'unica differenza è .catch() su una promise: il parametro della sua callback è tipizzato any, non unknown, quindi annotalo tu (.catch((e: unknown) => ...)). Vedi async/await per degli esempi.
Errori comuni
- Leggere
e.messagesenza restringere. Constrictnon compila; senzastrictsi rompe quando viene lanciato qualcosa che non è unError. - Catturare tutto e andare avanti. Gestisci gli errori che ti aspetti e rilancia gli altri.
- Dimenticare
namenegli errori personalizzati. I log diranno alloraErrorper tutti. - Lanciare stringhe.
throw "failed"non ha stack trace e non supera i controlliinstanceof Error.
Domande frequenti
Di che tipo è l'errore in un blocco catch di TypeScript?
unknown quando strict è attivo (il flag useUnknownInCatchVariables), altrimenti any. JavaScript può lanciare qualsiasi valore, non solo oggetti Error, quindi TypeScript ti obbliga a controllare. Restringilo con if (e instanceof Error) prima di leggere e.message.
Posso tipizzare la variabile del catch come Error in TypeScript?
No. catch (e: Error) è l'errore TS1196: Catch clause variable type annotation must be 'any' or 'unknown' if specified. Sono ammessi solo unknown e any, perché nulla garantisce cosa sia stato lanciato. Restringi invece dentro il blocco.
Come si lancia un errore in TypeScript?
Come in JavaScript: throw new Error("message"), o un'istanza di una sottoclasse predefinita o personalizzata come new RangeError(...). TypeScript ti lascia lanciare qualsiasi valore, ma lanciare oggetti Error conserva lo stack trace e fa funzionare i controlli instanceof Error.
Come si crea una classe di errore personalizzata in TypeScript?
Estendi Error, chiama super(message, options) e imposta name: class NotFoundError extends Error { name = "NotFoundError"; }. I campi extra vanno nel costruttore. Con qualsiasi target supportato (ES2015 e successivi), instanceof NotFoundError funziona senza correzioni al prototipo.
TypeScript ha eccezioni controllate o una clausola throws?
No. Il tipo di una funzione non dice nulla su cosa può lanciare, e il compilatore non verifica mai che gli errori vengano gestiti. Per gli errori che chi chiama deve gestire, restituiscili come valori con un tipo Result (una discriminated union di successo e fallimento), così il sistema di tipi li controlla davvero.