fetch APIIn JavaScript, poiché il motore è single-threaded, un compito pesante occupa interamente il thread principale, impedendo al browser di gestire eventi dell’utente (come i click) o di aggiornare l’interfaccia finché il compito non è terminato.
console.log('1. Richiesta dati avviata...');
const xhr = new XMLHttpRequest();
// Il terzo parametro 'false' rende la chiamata SINCRONA
xhr.open('GET', 'https://httpbin.org/delay/5', false);
xhr.send(null); // Il thread si FERMA qui per 5 secondi!
if (xhr.status === 200) {
console.log('2. Dati ricevuti:', xhr.responseText);
}
console.log('3. Ora il codice può finalmente proseguire');console.log('1. Richiesta dati avviata...');
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://httpbin.org/delay/5', true); // ASINCRONA!
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) { // Completata
if (xhr.status === 200) {
console.log('2. Dati ricevuti:', xhr.responseText);
}
}
};
xhr.send(null); // Non blocca!
console.log('3. Codice prosegue immediatamente!');
console.log('4. UI reattiva subito!');È chiaro che questo comportamento è indesiderabile.
La soluzione è delegare l’I/O al browser in background tramite un’operazione asincrona.
Come abbiamo visto con XMLHttpRequest, le operazioni asincrone richiedono di passare una callback che viene eseguita quando l’operazione è completata.
Ad esempio, proviamo a caricare uno script dinamicamente all’interno di una pagina:
In questo modo, stiamo iniettando lo script, ma non abbiamo modo di sapere quando è stato caricato e se è stato caricato correttamente, dunque rischiamo di eseguire codice che dipende dallo script prima che sia pronto, causando errori.
Come abbiamo visto con XMLHttpRequest, le operazioni asincrone richiedono di passare una callback che viene eseguita quando l’operazione è completata.
Ad esempio, proviamo a caricare uno script dinamicamente all’interno di una pagina:
Utilizzando la proprietà onload dell’elemento <script>, possiamo eseguire una callback quando lo script è stato caricato correttamente.
Però se volessimo caricare più script in sequenza?
Come abbiamo visto con XMLHttpRequest, le operazioni asincrone richiedono di passare una callback che viene eseguita quando l’operazione è completata.
Ad esempio, proviamo a caricare uno script dinamicamente all’interno di una pagina:
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}
loadScript('/my/script.js', () => {
console.log('Script caricato!');
loadScript('/my/other-script.js', () => {
console.log('Secondo script caricato!');
loadScript('/my/third-script.js', () => {
console.log('Terzo script caricato!');
});
});
});Utilizzando la proprietà onload dell’elemento <script>, possiamo eseguire una callback quando lo script è stato caricato correttamente.
Invocando loadScript all’interno della callback del precedente, possiamo caricare più script in sequenza, ma questo porta a un problema di composizione…
Caricare più script in sequenza con callback, soprattutto in presenza di logica strutturata, diventa rapidamente illeggibile:
loadScript('/script1.js', (err1, script1) => {
if (err1) { handleError(err1); }
else {
loadScript('/script2.js', (err2, script2) => {
if (err2) { handleError(err2); }
else {
loadScript('/script3.js', (err3, script3) => {
if (err3) { handleError(err3); }
else {
// Finalmente posso usare tutti gli script!
}
});
}
});
}
});Questo stile di codice è spesso chiamato callback hell o pyramid of doom a causa della struttura a piramide che si forma con i callback annidati.
Un approccio più moderno per gestire l’asincronia è utilizzare il pattern promise.
Una Promise è un oggetto JavaScript usato per gestire operazioni asincrone.
Esso rappresenta il risultato futuro di un’operazione asincrona che non è ancora disponibile, ma che lo sarà in un momento successivo.
Può avere tre stati mutuamente esclusivi:
| Stato | Quando | Proprietà |
|---|---|---|
| Pending | Operazione in corso | Non completata ancora |
| Fulfilled | Operazione riuscita | Ha un valore (result) |
| Rejected | Operazione fallita | Ha un errore (reason) |
Una Promise è immutabile!
Una volta risolta o rigettata, rimane in quello stato per sempre.
PromiseLe Promise vengono create con il costruttore new Promise(executor):
const myPromise = new Promise((resolve, reject) => {
// executor function - eseguita IMMEDIATAMENTE
console.log('Executor in esecuzione');
// Simulazione operazione asincrona
setTimeout(() => {
const success = true;
if (success) {
resolve('Operazione completata!'); // → Fulfilled
} else {
reject(new Error('Operazione fallita')); // → Rejected
}
}, 1000);
});
console.log('Promise creata (pending)');
// Output:
// Executor in esecuzione
// Promise creata (pending)
// (dopo 1s) → resolve() viene chiamatoL’executor viene eseguito immediatamente, mentre resolve/reject vengono chiamati dopo (asincrono).
Possiamo sempre convertire una funzione callback-based in una che ritorna Promise:
// Versione callback (vecchia)
1loadScript('/my/script.js', (err, script) => {
if (err) console.error(err);
else console.log('Caricato:', script.src);
});
// Versione Promise (moderna)
2function loadScriptPromise(src) {
return new Promise((resolve, reject) => {
3 loadScript(src, (err, script) => {
if (err) reject(err);
else resolve(script);
});
});
}promisifyObiettivo: creare un helper generico
Scrivi una funzione promisify(f) che:
f (error-first) con gli stessi argomenti di f (tranne la callback, ovviamente)Promisecallback(err, result) in reject/resolveEsempio di uso:
Il metodo .then(callback) viene utilizzato per registrare una callback che viene eseguita in caso di fulfill:
const promise = new Promise((resolve) => {
setTimeout(() => resolve('Dati ricevuti!'), 1000);
});
promise.then((result) => {
console.log('Successo:', result); // Eseguito quando risolto
});
console.log('Promessa in attesa...');
// Output:
// Promessa in attesa...
// (dopo 1s) Successo: Dati ricevuti!Il metodo .catch(callback) viene utilizzato per registrare una callback che viene eseguita in caso di reject:
const promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Errore di rete!')), 1000);
});
promise
.then(result => console.log('Dati:', result))
.catch((error) => {
console.error('Errore catturato:', error.message);
});
// Output (dopo 1s):
// Errore catturato: Errore di rete!Il metodo .finally() registra una callback che viene eseguita indipendentemente da successo o errore:
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve('OK'), 1000);
});
promise
.then(result => console.log('Successo:', result))
.catch(error => console.error('Errore:', error))
.finally(() => {
console.log('Operazione terminata'); // Eseguito SEMPRE
});
// Output (dopo 1s):
// Successo: OK
// Operazione terminataIl caso d’uso più frequente è il cleanup: chiudere connessioni, fermare spinner di caricamento, etc.
In alcuni casi vogliamo eseguire più operazioni asincrone in parallelo con diversi comportamenti:
Promise.all(...) attende che tutte le promise si risolvano:
Promise.race(...) restituisce la prima completata:
Un caso d’uso di Promise.race è implementare timeout per operazioni asincrone.
Le promise hanno migliorato il callback hell, ma con molte operazioni asincrone, anche le catene di .then() diventano verbose e difficili da seguire.
Inoltre, presentano alcuni comportamenti non intuitivi che possono causare bug.
Vediamone alcuni…
Cosa c’è che non va questo codice?
Osserva questo codice:
Output:
Creando promise per 1000ms ← Prima delay
Creando promise per 2000ms ← Seconda delay parte SUBITO!
Cosa notiamo?
Qual è il problema?
Il problema è sempre quello: tutte le promise partono subito al primo ciclo ed eseguono in parallelo!
Per eseguirle sequenzialmente dovremmo invece concatenarle con .then():
Output:
Creando promise per 1000ms
Creando promise per 1000ms
Creando promise per 1000ms
Step 0 ← Dopo 1s
Step 1 ← Dopo altri 1s
Step 2 ← Dopo altri 1s
Diventa molto più verboso e difficile da leggere!
Trattandosi di un problema comune, molti linguaggi di programmazione hanno introdotto costrutti per semplificare la scrittura di codice asincrono.
A partire da ES2017, JavaScript introduce le keyword async e await per risolvere questi problemi:
Leggibilità: sintassi lineare che sembra codice sincrono, più facile da leggere e mantenere
Controllo dell’esecuzione: await pausa l’esecuzione finché la promise non si risolve
Loop sequenziali: for/while + await funzionano come ci aspettiamo
Parametri naturali: non serve wrappare in arrow function
Vediamo il confronto diretto…
Promise
// Sequenza
delay(1000)
.then(() => delay(2000))
.then(() => console.log('Fine'));
// Loop parallelo
for (let i = 0; i < 3; i++) {
delay(1000).then(() =>
console.log(`Step ${i}`)
);
}
// Loop sequenziale
let promise = Promise.resolve();
for (let i = 0; i < 3; i++) {
promise = promise.then(() =>
delay(1000).then(() =>
console.log(`Step ${i}`)
)
);
}asyncLa keyword async può essere anteposta alla dichiarazione di una funzione.
Una funzione async esegue automaticamente il wrap del suo valore di ritorno in una Promise risolta:
Similmente, qualsiasi errore lanciato viene trasformato in una Promise rigettata:
awaitLa keyword await può essere anteposta a qualsiasi espressione che ritorna una Promise per sospendere l’esecuzione in attesa di un risultato.
In particolare, essa permette di restituire il controllo all’event loop finché la Promise non è risolta o rigettata, senza bloccare il thread principale.
await può essere usata solo all’interno di funzioni async (o anche top-level in browser moderni).
PromiseCome async esegue il wrap automatico del risultato in una Promise, await invece esegue l’unwrap automatico, restituendo direttamente il valore risolto:
await restituisce il valoreawait lancia un’eccezioneasync function fetchData(url) {
try {
const response = await fetch(url);
// ↑ await estrae response dalla Promise
const data = await response.json();
// ↑ await estrae data dalla Promise
return data; // Ritorna il valore estratto
} catch (error) {
// ↑ Cattura eccezioni da promise rigettate
console.error('Errore:', error.message);
}
}Il risultato è un codice più lineare e facile da leggere, senza callback annidati o catene di .then(); risulta molto più simile a codice sincrono, ma con tutti i vantaggi dell’asincronia.
throwIn JavaScript, quando qualcosa va storto, usiamo throw per lanciare un errore:
throw interrompe l’esecuzione e passa il controllo al blocco catch più vicino:
function validaEta(eta) {
if (typeof eta !== 'number') {
throw new TypeError('Eta deve essere un numero'); // ← Interrompe qui
}
if (eta < 0 || eta > 150) {
throw new RangeError('Eta deve essere tra 0 e 150');
}
return eta;
}
validaEta('ventiquattro'); // TypeError lanciato
console.log('Questo non viene eseguito');Senza gestione, il programma “muore” e l’errore compare in console.
try e catchCon try/catch, il programma non crasha e possiamo gestire l’errore:
try {
const valore = validaEta(-5); // ← Lancia RangeError
console.log('Eta valida:', valore); // Non eseguito
} catch (error) {
// ← Catturiamo l'errore qui
console.error(`${error.name}: ${error.message}`);
// RangeError: Eta deve essere tra 0 e 150
}
console.log('Programma continua normalmente!'); // ← Eseguito!finallyIl blocco finally viene eseguito sempre, indipendentemente da errore o successo:
try {
const valore = validaEta(25);
console.log('Eta valida:', valore);
} catch (error) {
console.error('Errore:', error.message);
} finally {
console.log('Validazione terminata'); // Sempre eseguito
}
// Output (successo):
// Eta valida: 25
// Validazione terminata
// Output (errore):
// Errore: Eta deve essere tra 0 e 150
// Validazione terminataCaso d’uso: ripulire risorse (fermare spinner, chiudere connessioni, etc.)
Combiniamo validazione, throw e catch per gestire dati incompleti:
function readUser(json) {
let user = JSON.parse(json); // Può lanciare SyntaxError
if (!user.name) {
throw new Error('Dati incompleti: manca name'); // ← Throw personalizzato
}
if (!user.age) {
throw new Error('Dati incompleti: manca age');
}
return user;
}
try {
const user = readUser('{ "age": 30 }'); // JSON valido ma incompleto
} catch (error) {
console.error(error.message); // Dati incompleti: manca name
}async/awaitCon async/await, usiamo lo stesso try/catch/finally per gestire errori:
async function fetchUserSafe(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`); // ← throw come in codice sincrono
}
const user = await response.json();
return user;
} catch (error) {
// ← Cattura sia errori lanciati che promise rigettate
console.error('Errore:', error.message);
return null;
} finally {
console.log('Fetch completato'); // Sempre eseguito
}
}
fetchUserSafe(999);
// Output:
// Errore: HTTP 404
// Fetch completatoI pattern sono identici tra codice sincrono e asincrono!
Abbiamo già visto XMLHttpRequest. fetch è l’alternativa moderna:
| Aspetto | XHR | Fetch |
|---|---|---|
| Base | Callback/onload | Promise-based |
| Sintassi | Verbosa | Compatta |
| Errori | onload/onerror | .then()/.catch() |
| Body | responseText | response.json() |
response.ok e response.statusImportante: Fetch non lancia errore per codici di errore HTTP (e.g. 404, 500, etc.)
Dobbiamo controllare manualmente:
async function fetchSafe(url) {
1 const response = await fetch(url);
console.log(response.status); // 200, 404, 500, etc.
console.log(response.ok); // true se 200-299, false
console.log(response.statusText); // 'OK', 'Not Found', etc.
2 if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
3 return await response.json();
}
fetchSafe('/api/todos/99999').catch(err => {
console.error('Errore:', err.message); // HTTP 404
});response.ok (true se 200-299)
Best practice: controllare response.ok PRIMA di .json()
Creare una classe HttpError per distinguere errori HTTP da altri errori:
1class HttpError extends Error {
constructor(response) {
super(`${response.status} for ${response.url}`);
this.name = 'HttpError';
this.response = response;
}
}
2async function loadJson(url) {
const response = await fetch(url);
if (!response.ok) {
3 throw new HttpError(response);
}
return await response.json();
}Error
response.ok
HttpError se status non ok
// Uso
loadJson('/api/user/invalid')
.then(user => console.log(user.name))
.catch(err => {
4 if (err instanceof HttpError) {
console.error('HTTP Error:', err.message);
} else {
console.error('Other Error:', err);
}
});async function createTodo(title, userId) {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/todos', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: title,
completed: false,
userId: userId
})
});
if (!response.ok) { throw new Error(`HTTP ${response.status}`); }
const newTodo = await response.json();
console.log('Creato:', newTodo.id);
return newTodo;
} catch (error) {
console.error('Errore creazione:', error);
}
}
createTodo('Imparare async/await', 1);async function updateTodo(todoId, updates) {
try {
const response = await fetch(`/api/todos/${todoId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const updated = await response.json();
console.log('Aggiornato:', updated.id);
return updated;
} catch (error) {
console.error('Errore aggiornamento:', error);
}
}
updateTodo(1, { completed: true, title: 'Task completato' });async function deleteTodo(todoId) {
try {
const response = await fetch(`/api/todos/${todoId}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
// DELETE spesso ritorna 204 (No Content)
console.log('Eliminato:', todoId);
return { success: true };
} catch (error) {
console.error('Errore eliminazione:', error);
}
}
deleteTodo(1);Niccolò Maltoni