Cancelando procesos asincrónicos en JavaScript
Cancelando procesos asincrónicos en JavaScript
En operaciones asincrónicas normalmente pasa que necesitamos cancelar la misma mientras que estamos esperando la respuesta. El caso más claro de esto es cuando hacemos un request a una API y esta demora más de lo esperado. Para poder solucionar esto surgieron las AbortSignal
y el AbortController
que nos brindan de una potente herramienta para cortar un proceso asincrónico de forma eficiente y segura.
¿Qué son AbortSignal y AbortController?
AbortSignal
y AbortController
son parte de la especificación del DOM, para abortar actividades en proceso, que proporciona una forma de notificar a las operaciones asíncronas que deben parar su proceso normal y terminar. AbortSignal
es una interfaz que representa una señal de aborto, que puede ser utilizada por las operaciones asíncronas para verificar si se ha solicitado la cancelación de la operación. Por otro lado, AbortController
es una clase que nos permite crear una señal de aborto (AbortSignal
) y controlar su estado. Podemos usar un AbortController
para enviar una señal de aborto a una operación asíncrona en cualquier momento.
Cómo usar AbortSignal y AbortController con fetch
Nada mejor que un ejemplo para entender mejor de que estamos hablando con estas dos herramientas y que mejor ejemplo que haciendo un request a una API usando fetch
.
// Create an instance of AbortController
const controller = new AbortController();
const signal = controller.signal;
// Set a timer to abort the request after 5 seconds
setTimeout(() => controller.abort(), 5000);
// Make a network request with fetch
fetch("https://api.example.com/data", { signal })
.then((response) => {
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.json();
})
.then((data) => console.log(data))
.catch((error) => {
if (error.name === "AbortError") {
console.log("The request was aborted");
} else {
console.error("There was an error:", error.message);
}
});
En este ejemplo, creamos una instancia de AbortController
y obtenemos su señal (signal
). Luego, configuramos un temporizador para abortar la solicitud después de 5 segundos utilizando controller.abort()
. Por último, pasamos la señal al método fetch
(como uno de los atributos del objeto del parámetro de opciones) para que pueda ser utilizada para abortar la solicitud si es necesario. Algo interesante para tener en cuenta es que se podría usar la misma señal en múltiples funciones y así asegurar que el proceso total no supere más de 5 minutos.
Hay una opción más simple de hacer lo mismo sin necesidad del controller, usando el método estatico AbortSignal.timeout()
. Este método devuelve una señal que se abortará automáticamente cuando transcurran los milisegundos pasados por parámetro (similar al controller con el setTimeout creado en el ejemplo anterior). Algo para tener en cuenta es que por mas que timeout ya está presente hace un tiempo podríamos tener una excepción tipo TypeError
si no está implementado el método.
const url = "video.mp4";
try {
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
const result = await res.blob();
// …
} catch (err) {
if (err.name === "TimeoutError") {
console.error("Timeout: It took more than 5 seconds to get the result!");
} else if (err.name === "AbortError") {
console.error(
"Fetch aborted by user action (browser stop button, closing tab, etc.",
);
} else if (err.name === "TypeError") {
console.error("AbortSignal.timeout() method is not supported");
} else {
// A network error, or some other problem.
console.error(`Error: type: ${err.name}, message: ${err.message}`);
}
}
Otros escenarios más genéricos
Estas herramientas no solo se usan junto a fetch
sino que se pueden aprovechar en cualquier llamada asincrónica. Podemos encontrar el siguiente ejemplo en la especificación donde se tiene un spinner y una función asincrónica doAmazingness
y donde simplemente se aborta la señal ante un click en un botón.
const controller = new AbortController();
const signal = controller.signal;
startSpinner();
doAmazingness({ ..., signal })
.then(result => ...)
.catch(err => {
if (err.name == 'AbortError') return;
showUserErrorMessage();
})
.then(() => stopSpinner());
// …
abortBtnElement.addEventListener('click', () => {
controller.abort();
});
La función doAmazingness
podría ser cualquier lógica asincrónica que tengamos, incluyendo crear una Promise
y donde podemos tirar una excepción cuando la señal pase a estar abortada o podemos escuchar el evento de abort de la misma y hacer un reject de la promise.
function doAmazingness({signal}) {
return new Promise((resolve, reject) => {
signal.throwIfAborted();
// Begin doing amazingness, and call resolve(result) when done.
// But also, watch for signals:
signal.addEventListener('abort', () => {
// Stop doing amazingness, and:
reject(signal.reason);
});
});
}
Usando múltiples signals
Un escenario más completo podría considerar un tiempo máximo de timeout y aparte una operación de usuario (como clickear en un botón para cancelar el request). En estos escenarios necesitamos combinar las señales en una sola, para que cuando cualquiera de las mismas se dispare, abortar el proceso. Para esto tenemos disponible AbortSignal.any()
.
try {
const controller = new AbortController();
const timeoutSignal = AbortSignal.timeout(5000);
const res = await fetch(url, {
// This will abort the fetch when either signal is aborted
signal: AbortSignal.any([controller.signal, timeoutSignal]),
});
const body = await res.json();
} catch (e) {
if (e.name === "AbortError") {
// Notify the user of abort.
} else if (e.name === "TimeoutError") {
// Notify the user of timeout
} else {
// A network error, or some other problem.
console.log(`Type: ${e.name}, Message: ${e.message}`);
}
}
Conclusiones
El uso de AbortSignal
y AbortController
nos permite tener un mayor control sobre las operaciones asíncronas en JavaScript. Nos permite cancelar las operaciones en cualquier momento, lo que puede ser útil en situaciones donde necesitamos optimizar el rendimiento o manejar mejor los recursos del cliente. Es una herramienta poderosa que deberíamos considerar utilizar en nuestras aplicaciones web modernas.