La gestione degli errori è un aspetto fondamentale della programmazione che garantisce che le applicazioni possano affrontare con grazia situazioni inaspettate. In JavaScript, mentre try-catch
è comunemente utilizzato, ci sono tecniche più avanzate per migliorare la gestione degli errori.
Questo articolo esplora questi metodi avanzati, fornendo soluzioni pratiche per migliorare le tue strategie di gestione degli errori e rendere le tue applicazioni più resilienti.
Che cos’è la gestione degli errori?
Lo scopo della gestione degli errori
La gestione degli errori anticipa, rileva e risponde a problemi che sorgono durante l’esecuzione del programma. Una corretta gestione degli errori migliora l’esperienza dell’utente, mantiene la stabilità dell’applicazione e garantisce affidabilità.
Tipi di errori in JavaScript
- Errori di sintassi. Questi sono errori nella sintassi del codice, come parentesi mancanti o uso errato di parole chiave.
- Errori di runtime. Si verificano durante l’esecuzione, come l’accesso a proprietà di oggetti indefiniti.
- Errori logici. Questi errori non causano il crash del programma ma portano a risultati errati, spesso a causa di logica difettosa o effetti collaterali indesiderati.
Perché try-catch non è sufficiente
Le limitazioni di try-catch
- Limitazioni di ambito. Gestisce solo il codice sincrono all’interno del suo blocco e non influisce sulle operazioni asincrone a meno che non siano gestite specificamente.
- Guasti silenziosi. L’uso eccessivo o improprio può portare a errori che vengono silenziosamente ignorati, causando potenzialmente comportamenti inaspettati.
- Propagazione degli errori. Non supporta nativamente la propagazione degli errori attraverso diversi livelli dell’applicazione.
Quando usare il blocco try-catch
- Codice sincrono. Efficace per gestire errori in operazioni sincrone come il parsing JSON.
- Sezioni critiche. Usare per proteggere sezioni di codice critiche dove gli errori possono avere conseguenze gravi.
Classi di Errori Personalizzate: Miglioramento delle Informazioni sugli Errori
Creazione di una Classe di Errore Personalizzata
Le classi di errori personalizzate estendono la classe integrata Error
per fornire informazioni aggiuntive:
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
this.stack = (new Error()).stack; // Capture the stack trace
}
}
Vantaggi degli Errori Personalizzati
- Chiarezza. Offre messaggi di errore specifici.
- Gestione granulare. Consente di gestire separatamente tipi di errore specifici.
- Metadati degli errori. Include contesto aggiuntivo sull’errore.
Casi d’Uso per Errori Personalizzati
- Guasti di convalida. Errori relativi alla convalida dell’input dell’utente.
- Errori specifici del dominio. Errori personalizzati per specifici domini applicativi come autenticazione o elaborazione dei pagamenti.
Gestione Centralizzata degli Errori
Gestione Globale degli Errori in Node.js
Centralizzare la gestione degli errori utilizzando middleware:
app.use((err, req, res, next) => {
console.error('Global error handler:', err);
res.status(500).json({ message: 'An error occurred' });
});
Gestione Centralizzata degli Errori nelle Applicazioni Frontend
Implementare la gestione centralizzata degli errori in React utilizzando i limiti degli errori:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by ErrorBoundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
Vantaggi della Gestione Centralizzata degli Errori
- Coerenza. Garantisce un approccio uniforme alla gestione degli errori.
- Migliore manutenzione. Gli aggiornamenti centralizzati riducono il rischio di perdere modifiche.
- Migliori registrazioni e monitoraggio. Agevola l’integrazione con gli strumenti di monitoraggio.
Propagare gli Errori
Propagazione degli Errori nel Codice Sincrono
Usare throw
per propagare gli errori:
function processData(data) {
try {
validateData(data);
saveData(data);
} catch (error) {
console.error('Failed to process data:', error);
throw error;
}
}
Propagazione degli Errori nel Codice Asincrono
Gestire gli errori con le promesse o async
/await
:
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Failed to fetch data:', error);
throw error;
}
}
Quando Propagare gli Errori
- Errori critici. Propagare gli errori che influenzano l’intera applicazione.
- Logica di business. Consentire ai componenti di livello superiore di gestire gli errori della logica di business.
Gestione degli Errori nel Codice Asincrono
Gestione degli Errori con async/await
Usare try-catch
per gestire gli errori nelle funzioni asincrone:
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user data');
}
return await response.json();
} catch (error) {
console.error('Error fetching user data:', error);
return null;
}
}
Utilizzo di Promise.all con Gestione degli Errori
Gestire più promesse e errori:
async function fetchMultipleData(urls) {
try {
const responses = await Promise.all(urls.map
(url => fetch(url)));
return await Promise.all(responses.map(response => {
if (!response.ok) {
throw new Error(`Failed to fetch ${response.url}`);
}
return response.json();
}));
} catch (error) {
console.error('Error fetching multiple data:', error);
return [];
}
}
Trappole Comuni nella Gestione degli Errori Asincroni
- Promesse non gestite. Gestire sempre le promesse utilizzando
await
,.then()
o.catch()
. - Errori silenziosi. Assicurarsi che gli errori non vengano silenziosamente ignorati.
- Condizioni di gara. Fai attenzione con le operazioni asincrone concorrenti.
Registrazione degli errori
Registrazione degli errori lato client
Cattura errori globali:
window.onerror = function(message, source, lineno, colno, error) {
console.error('Global error captured:', message, source, lineno, colno, error);
sendErrorToService({ message, source, lineno, colno, error });
};
Registrazione degli errori lato server
Utilizza strumenti come Winston per la registrazione lato server:
const winston = require('winston');
const logger = winston.createLogger({
level: 'error',
format: winston.format.json(),
transports: [new winston.transports.File({ filename: 'error.log' })]
});
app.use((err, req, res, next) => {
logger.error(err.stack);
res.status(500).send('An error occurred');
});
Monitoraggio e avvisi
Configura monitoraggio in tempo reale e avvisi con servizi come PagerDuty o Slack:
function notifyError(error) {
// Send error details to monitoring service
}
Best Practices per la Registrazione degli Errori
- Includi contesto. Registra informazioni aggiuntive come dati di richiesta e informazioni sugli utenti.
- Evita il sovra-logging. Registra informazioni essenziali per prevenire problemi di prestazioni.
- Analizza i log regolarmente. Rivedi regolarmente i log per rilevare e affrontare problemi ricorrenti.
Degradazione Elegante e Fallback
Degradazione Elegante
Progetta la tua applicazione per continuare a funzionare con capacità ridotte:
function renderProfilePicture(user) {
try {
if (!user.profilePicture) {
throw new Error('Profile picture not available');
}
return `<img data-fr-src="${user.profilePicture}" alt="Profile Picture">`;
} catch (error) {
console.error('Error rendering profile picture:', error.message);
return '<img src="/default-profile.png" alt="Default Profile Picture">';
}
}
Meccanismi di Fallback
Fornisci alternative quando le operazioni principali falliscono:
async function fetchDataWithFallback(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return await response.json();
} catch (error) {
console.error('Error fetching data:', error);
return { message: 'Default data' }; // Fallback data
}
}
Implementazione della Degradazione Elegante
- Fallback dell’interfaccia utente. Fornisci elementi alternativi dell’interfaccia utente quando le funzionalità falliscono.
- Fallback dei dati. Utilizza valori memorizzati nella cache o predefiniti quando i dati live non sono disponibili.
- Meccanismi di riprova. Implementa logica di riprova per errori transitori.
Bilanciare la Degradazione Elegante
Bilanciare la fornitura di fallback mantenendo gli utenti informati sui problemi:
function showErrorNotification(message) {
// Notify users about the issue
}
Test di Gestione degli Errori
Test Unitari della Gestione degli Errori
Verificare la gestione degli errori nelle singole funzioni:
const { validateUserInput } = require('./validation');
test('throws error for invalid username', () => {
expect(() => {
validateUserInput({ username: 'ab' });
}).toThrow('Username must be at least 3 characters long.');
});
Test di Integrazione
Testare la gestione degli errori attraverso diversi livelli dell’applicazione:
test('fetches data with fallback on error', async () => {
fetch.mockReject(new Error('Network error'));
const data = await fetchDataWithFallback('https://api.example.com/data');
expect(data).toEqual({ message: 'Default data' });
});
Test end-to-end
Simulare scenari del mondo reale per testare la gestione degli errori:
describe('ErrorBoundary', () => {
it('displays error message on error', () => {
cy.mount(<ErrorBoundary><MyComponent /></ErrorBoundary>);
cy.get(MyComponent).then(component => {
component.simulateError(new Error('Test error'));
});
cy.contains('Something went wrong.').should('be.visible');
});
});
Linee Guida per i Test della Gestione degli Errori
- Coprire casi limite. Assicurarsi che i test affrontino vari scenari di errore.
- Testare i fallback. Verificare che i meccanismi di fallback funzionino come previsto.
- Automatizzare i test. Utilizzare i pipeline CI/CD per automatizzare e garantire una gestione degli errori robusta.
Scenari del Mondo Reale
Scenario 1: Sistema di Elaborazione dei Pagamenti
Gestire gli errori durante l’elaborazione dei pagamenti:
- Classi di errore personalizzate. Utilizzare classi come
CardValidationError
,PaymentGatewayError
. - Logica di ripetizione. Implementare ripetizioni per problemi legati alla rete.
- Registrazione centralizzata. Monitorare gli errori di pagamento e affrontare prontamente i problemi.
Scenario 2: Applicazioni ad Alto Contenuto di Dati
Gestire gli errori nell’elaborazione dei dati:
- Degradazione graziosa. Fornire dati parziali o visualizzazioni alternative.
- Dati di fallback. Utilizzare valori memorizzati nella cache o predefiniti.
- Registrazione degli errori. Registra contesti dettagliati per la risoluzione dei problemi.
Scenario 3: Autenticazione e Autorizzazione dell’Utente
Gestire gli errori di autenticazione e autorizzazione:
- Classi di errore personalizzate. Creare classi come
AuthenticationError
,AuthorizationError
. - Gestione centralizzata. Registra e monitora problemi relativi all’autenticazione.
- Degradazione delicata. Offri opzioni di accesso alternative e messaggi di errore significativi.
Conclusioni
La gestione avanzata degli errori in JavaScript richiede di andare oltre il semplice try-catch
per abbracciare errori personalizzati, gestione centralizzata, propagazione e test robusti. Implementando queste tecniche è possibile costruire applicazioni resilienti che offrono un’esperienza utente senza soluzione di continuità, anche quando le cose vanno male.
Ulteriori Letture
- “JavaScript: The Good Parts” di Douglas Crockford
- “You Don’t Know JS: Async & Performance” di Kyle Simpson
- Documentazione Web di MDN: Gestione degli Errori
Source:
https://dzone.com/articles/advanced-error-handling-in-javascript-custom-error