Gestionarea erorilor în TypeScript
Gestionarea corectă a erorilor este esențială pentru a construi aplicații TypeScript fiabile.
Această pagină pornește de la blocul de bază try/catch și ajunge la modele mai avansate: clase de eroare personalizate, gardieni de tip, erori asincrone și bune practici.
Gestionarea de bază a erorilor
Codul care poate eșua se pune într-un bloc try. Dacă acolo este aruncat ceva, execuția sare la blocul catch, unde primești valoarea aruncată.
Notă: în TypeScript 4.0 și mai nou (cu useUnknownInCatchVariables, pe care strict îl activează) variabila din catch are tipul unknown. Restrânge-i întotdeauna tipul înainte de a citi proprietăți precum message.
Blocuri try/catch
Fundamentul gestionării erorilor în TypeScript: divide aruncă o eroare la împărțirea la zero, iar blocul catch o raportează.
divide(a: number, b: number): number care aruncă new Error("Împărțire la zero") atunci când b este 0. Apelează divide(10, 0) într-un bloc try care ar afișa rezultatul, iar în catch afișează A apărut o eroare: urmat de mesajul erorii, folosind console.error.
Ieșire: program TypeScript și rezultatul capturat
function divide(a: number, b: number): number {
if (b === 0) {
throw new Error('Împărțire la zero');
}
return a / b;
}
try {
const result = divide(10, 0);
console.log(result);
} catch (error) {
console.error('A apărut o eroare:', error.message);
}
Clase de eroare personalizate
Poți extinde clasa încorporată Error pentru a crea erori care poartă informații suplimentare pentru domeniul tău, cum ar fi câmpul care a picat validarea sau un cod de eroare din baza de date.
Crearea claselor de eroare personalizate
ValidationError și DatabaseError extind Error, își stabilesc propriul name și restaurează lanțul de prototipuri, astfel încât instanceof să funcționeze și după compilarea către ținte mai vechi.
Error.
Definește ValidationError (mesaj plus un câmp public opțional field) și DatabaseError (mesaj plus un câmp public numeric code); ambele stabilesc this.name și restaurează lanțul de prototipuri cu Object.setPrototypeOf. Adaugă o funcție validateUser(user: any) care aruncă o ValidationError pentru un nume lipsă (câmpul name) sau pentru un email fără @ (câmpul email). Doar declarații — nu se afișează nimic.
Ieșire: program TypeScript și rezultatul capturat
class ValidationError extends Error {
constructor(message: string, public field?: string) {
super(message);
this.name = 'ValidationError';
// Restaurăm lanțul de prototipuri
Object.setPrototypeOf(this, ValidationError.prototype);
}
}
class DatabaseError extends Error {
constructor(message: string, public code: number) {
super(message);
this.name = 'DatabaseError';
Object.setPrototypeOf(this, DatabaseError.prototype);
}
}
// Utilizare
function validateUser(user: any) {
if (!user.name) {
throw new ValidationError('Numele este obligatoriu', 'name');
}
if (!user.email.includes('@')) {
throw new ValidationError('Format de email invalid', 'email');
}
}
Gardieni de tip pentru erori
Deoarece o valoare capturată poate fi orice, scrie gardieni de tip — funcții care returnează error is SomeType — pentru a verifica ce ai capturat de fapt, înainte de a folosi valoarea.
Pentru cazuri mai complexe poți folosi și o funcție de aserțiune (asserts error is Error), arătată în fragmentul de mai jos: după apel, TypeScript tratează valoarea ca fiind Error.
Predicate de tip pentru gestionarea erorilor
Doi gardieni de tip permit blocului catch să trateze o ValidationError, orice obiect cu un message de tip șir de caractere sau o valoare complet necunoscută.
unknown.
Refolosește o clasă ValidationError (mesaj și câmpul opțional field) și o funcție validateUser care o aruncă atunci când name lipsește. Scrie isErrorWithMessage(error: unknown): error is { message: string } (obiect, diferit de null, care are un message de tip șir de caractere) și isValidationError(error: unknown): error is ValidationError folosind instanceof. Apelează validateUser({}) într-un bloc try; în catch (error: unknown) afișează Eroare de validare în <field>: <message> pentru erorile de validare, A apărut o eroare: <message> pentru alte erori care au un mesaj, sau A apărut o eroare necunoscută.
Ieșire: program TypeScript și rezultatul capturat
class ValidationError extends Error {
constructor(message: string, public field?: string) {
super(message);
this.name = 'ValidationError';
Object.setPrototypeOf(this, ValidationError.prototype);
}
}
function validateUser(user: any) {
if (!user.name) {
throw new ValidationError('Numele este obligatoriu', 'name');
}
if (!user.email.includes('@')) {
throw new ValidationError('Format de email invalid', 'email');
}
}
// Gardieni de tip
function isErrorWithMessage(error: unknown): error is { message: string } {
return (
typeof error === 'object' &&
error !== null &&
'message' in error &&
typeof (error as Record<string, unknown>).message === 'string'
);
}
function isValidationError(error: unknown): error is ValidationError {
return error instanceof ValidationError;
}
// Utilizare într-un bloc catch
try {
validateUser({});
} catch (error: unknown) {
if (isValidationError(error)) {
console.error(`Eroare de validare în ${error.field}: ${error.message}`);
} else if (isErrorWithMessage(error)) {
console.error('A apărut o eroare:', error.message);
} else {
console.error('A apărut o eroare necunoscută');
}
}
Funcție de aserțiune pentru erori
assertIsError aruncă o eroare dacă valoarea nu este un Error; altfel, TypeScript restrânge valoarea capturată la Error pentru restul blocului.
function assertIsError(error: unknown): asserts error is Error {
if (!(error instanceof Error)) {
throw new Error('Caught value is not an Error instance');
}
}
try {
// ...
} catch (error) {
assertIsError(error);
console.error((error as Error).message); // TypeScript now knows error is Error
}
Gestionarea erorilor asincrone
Cu async/await, erorile provenite din promisiuni așteptate sunt capturate de un try/catch obișnuit. Cu promisiuni simple, înlănțuiește în schimb un handler .catch().
Nu lăsa niciodată o respingere de promisiune netratată. Fie înlănțuiești .catch(), fie folosești void pentru a arăta că ignorarea rezultatului este intenționată, dar erorile sunt totuși capturate.
Tratarea erorilor async/await
Învelirea unui await în try/catch îți permite să înregistrezi eroarea și să o re-arunci; un lanț de promisiuni se poate reface cu o valoare de rezervă în .catch(). Apelul de rețea este înlocuit cu un simulacru local, astfel încât exemplul să ruleze oriunde.
.catch() pe promisiune.
Definește interfețele User și Post și o funcție locală mockFetch(url) care returnează un răspuns fals rezolvat (un utilizator John Doe pentru /api/users/1, o postare cu titlul Salut, lume pentru URL-ul /posts), astfel încât să nu se folosească rețeaua. Scrie fetchUser(userId): Promise<User> care așteaptă răspunsul, aruncă Eroare HTTP! status: <status> atunci când răspunsul nu este ok, afișează Preluarea utilizatorului a eșuat: în blocul său catch și re-aruncă eroarea; și fetchUserPosts(userId): Promise<Post[]> folosind .then() cu aceeași verificare și un .catch() care afișează Preluarea postărilor a eșuat: și returnează []. Apelează ambele funcții și afișează numele utilizatorului și tabloul de postări.
Ieșire: program TypeScript și rezultatul capturat
interface User {
id: number;
name: string;
email: string;
}
interface Post {
id: number;
title: string;
}
// Înlocuitor local pentru fetch, ca exemplul să ruleze fără rețea
function mockFetch(url: string): Promise<{ ok: boolean; status: number; json: () => Promise<any> }> {
if (url.endsWith('/posts')) {
return Promise.resolve({ ok: true, status: 200, json: async () => [{ id: 1, title: 'Salut, lume' }] });
}
return Promise.resolve({
ok: true,
status: 200,
json: async () => ({ id: 1, name: 'John Doe', email: 'john@example.com' })
});
}
// Folosind async/await cu try/catch
async function fetchUser(userId: number): Promise<User> {
try {
const response = await mockFetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`Eroare HTTP! status: ${response.status}`);
}
return await response.json() as User;
} catch (error) {
if (error instanceof Error) {
console.error('Preluarea utilizatorului a eșuat:', error.message);
}
throw error; // Re-aruncăm eroarea, ca apelantul să o poată trata
}
}
// Folosind Promise.catch() pentru tratarea erorilor
function fetchUserPosts(userId: number): Promise<Post[]> {
return mockFetch(`/api/users/${userId}/posts`)
.then(response => {
if (!response.ok) {
throw new Error(`Eroare HTTP! status: ${response.status}`);
}
return response.json();
})
.catch(error => {
console.error('Preluarea postărilor a eșuat:', error);
return []; // Returnăm un tablou gol ca valoare de rezervă
});
}
// Utilizare
fetchUser(1).then(user => console.log('Utilizator:', user.name));
fetchUserPosts(1).then(posts => console.log('Postări:', posts));
Tratarea respingerilor de promisiuni
O promisiune fără .catch() este o problemă care așteaptă să se întâmple; cele două forme corecte tratează respingerea sau ignoră explicit rezultatul cu void.
// Bad: Unhandled promise rejection
fetchData().then(data => console.log(data));
// Good: Handle both success and error cases
fetchData()
.then(data => console.log('Success:', data))
.catch(error => console.error('Error:', error));
// Or use void for intentionally ignored errors
void fetchData().catch(console.error);
Limite de eroare în React
Într-o aplicație React, o limită de eroare este o componentă de tip clasă care capturează erorile JavaScript aruncate oriunde în arborele componentelor sale copil și afișează o interfață de rezervă în loc să prăbușească întreaga pagină.
Componentă de limită de eroare React
O clasă ErrorBoundary cu props și stare tipizate, care reține eroarea în getDerivedStateFromError, o înregistrează în jurnal în componentDidCatch și randează o interfață de rezervă.
import React, { Component, ErrorInfo, ReactNode } from 'react';
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
public state: ErrorBoundaryState = {
hasError: false
};
public static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo);
// Log to error reporting service
}
public render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="error-boundary">
<h2>Something went wrong</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
// Usage
function App() {
return (
<ErrorBoundary fallback={<div>Oops! Something broke.</div>}>
<MyComponent />
</ErrorBoundary>
);
}
Bune practici
Tratează întotdeauna erorile. Nu lăsa niciodată un bloc catch gol — cel puțin înregistrează eroarea în jurnal.
Folosește tipuri de eroare specifice. Creează o clasă de eroare personalizată pentru fiecare fel de eșec, astfel încât apelanții să le poată deosebi.
Tratează erorile la nivelul potrivit. Capturează o eroare acolo unde ai suficient context ca să te poți reface sau ca să afișezi un mesaj util utilizatorului — adesea nu în aceeași funcție care o aruncă.
Nu lăsa niciodată catch gol
Un catch tăcut ascunde eșecurile; înregistrarea erorii în jurnal este minimul necesar.
// Bad: Silent failure
try { /* ... */ } catch { /* empty */ }
// Good: At least log the error
try { /* ... */ } catch (error) {
console.error('Operation failed:', error);
}
Clase de eroare specifice
NetworkError poartă un status HTTP, iar ValidationError numele câmpului care a picat validarea.
class NetworkError extends Error {
constructor(public status: number, message: string) {
super(message);
this.name = 'NetworkError';
}
}
class ValidationError extends Error {
constructor(public field: string, message: string) {
super(message);
this.name = 'ValidationError';
}
}
Tratarea erorilor la nivelul potrivit
Stratul de date aruncă o NetworkError; stratul de interfață o capturează și decide ce mesaj să afișeze.
// In a data access layer
async function getUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new NetworkError(response.status, 'Failed to fetch user');
}
return response.json();
}
// In a UI component
async function loadUser() {
try {
const user = await getUser('123');
setUser(user);
} catch (error) {
if (error instanceof NetworkError) {
if (error.status === 404) {
showError('User not found');
} else {
showError('Network error. Please try again later.');
}
} else {
showError('An unexpected error occurred');
}
}
}
Capcane frecvente
Netratarea respingerilor de promisiuni. Atașează întotdeauna un handler promisiunilor, altfel primești avertismente de respingere netratată și pierzi erori.
Capturarea fără restrângerea tipului. În TypeScript 4.0+ valoarea capturată este unknown; citirea lui error.message fără o verificare este o eroare de compilare.
Înghițirea erorilor. Nu captura o eroare doar ca să o ignori. Înregistreaz-o în jurnal și, acolo unde are sens, anunță utilizatorul.
Rezumat — gestionarea eficientă a erorilor în TypeScript înseamnă:
- Folosirea
try/catchpentru cod sincron - Tratarea respingerilor de promisiuni cu
.catch()sau cutry/catchîmpreună cuasync/await - Crearea de clase de eroare personalizate pentru erorile specifice domeniului
- Folosirea gardienilor de tip pentru a lucra în siguranță cu obiectele de eroare
- Tratarea erorilor la nivelul potrivit al aplicației
- Oferirea de mesaje de eroare utile utilizatorilor
Respingere de promisiune netratată
Apelarea unei funcții asincrone și abandonarea promisiunii, în comparație cu atașarea unui .catch().
// Bad: Unhandled promise rejection
fetchData();
// Good: Handle the rejection
fetchData().catch(console.error);
Capturare fără restrângerea tipului
Citirea lui message dintr-o eroare de tip unknown eșuează; o verificare instanceof Error o face sigură.
// Bad: Error is of type 'unknown'
try { /* ... */ } catch (error) {
console.log(error.message); // Error: Property 'message' does not exist on type 'unknown'
}
// Good: Narrow the type
try { /* ... */ } catch (error) {
if (error instanceof Error) {
console.log(error.message); // OK
}
}
Înghițirea erorilor
Prima funcție saveData ascunde eșecurile; a doua le înregistrează în jurnal și anunță utilizatorul.
// Bad: Error is silently ignored
function saveData(data: Data) {
try {
database.save(data);
} catch {
// Ignore
}
}
// Better: Log the error and/or notify the user
function saveData(data: Data) {
try {
database.save(data);
} catch (error) {
console.error('Failed to save data:', error);
showError('Failed to save data. Please try again.');
}
}