En la web, muchas cosas tienden a llevar mucho tiempo: si consultas una API, puede llevar un tiempo recibir una respuesta. Por lo tanto, la programación asíncrona es una habilidad esencial para los desarrolladores.
Cuando trabajamos con operaciones asíncronas en JavaScript, a menudo escuchamos el término Promise
(Promesa). Pero puede resultar complicado entender cómo funcionan y cómo utilizarlos.
A diferencia de muchos tutoriales de programación tradicionales, en este tutorial aprenderemos con la práctica. Completaremos cuatro tareas al final del artículo:
- Tarea 1: explicación de los conceptos básicos de la promesa usando mi cumpleaños.
- Tarea 2: construye un juego de adivinanzas.
- Tarea 3: obtener información del país de una API.
- Tarea 4: buscar países vecinos de un país.
Si deseas seguir adelante, asegúrate de descargar los recursos aquí: https://bit.ly/3m4bjWI
Tarea 1: explicación de los conceptos básicos de la promesa usando mi cumpleaños
Mi amiga Kayo promete hacer una torta para mi cumpleaños en dos semanas.
Si todo va bien, y Kayo no se enferma, tendremos una cierta cantidad de tortas. (Las tortas son contables en este tutorial ?). De lo contrario, si Kayo se enferma, no tendremos tortas.
De cualquier manera, todavía vamos a tener una fiesta. Para esta primera tarea, traduciremos esta historia en código. Primero, creemos una función que devuelva una Promise
:
const miCumple = (KayoSeEnferma) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (!KayoSeEnferma) {
resolve(2);
} else {
reject(new Error("Estoy triste"));
}
}, 2000);
});
};
En JavaScript, podemos crear una nueva Promise
con new Promise()
, que toma una función como argumento:(resolve, reject) => {}
.
En esta función, resolve
(resolver) y reject
(rechazar) son funciones callback, que se proporcionan de forma predeterminada en JavaScript.
Echemos un vistazo más de cerca al código anterior.
Cuando ejecutamos la función miCumple
, después de 2000 ms
:
- Si Kayo no está enferma, ejecutamos
resolve
con2
como argumento. - Si Kayo está enferma, ejecutamos el
reject
con unnew Error("Estoy triste")
como argumento. Aunque puedes pasar cualquier cosa areject
como argumento, se recomienda pasarle un objetoError
.
Ahora, porque miCumple()
retorna una Promise
, tenemos acceso a los métodos then
(entonces), catch
(atrapa), yfinally
(finalmente).
Y también tenemos acceso a los argumentos que se pasaron a resolve
y reject
antes en ese then
y catch
.
Echemos un vistazo más de cerca al código.
Si Kayo no está enferma:
miCumple(false)
.then((result) => {
console.log(`Yo tengo ${result} tortas`); // En la consola: Yo tengo 2 tortas
})
.catch((error) => {
console.log(error); // No se ejecuta
})
.finally(() => {
console.log("Fiesta"); // Aparece en la consola no importa qué: Fiesta
});
Si Kayo está enferma:
miCumple(true)
.then((result) => {
console.log(`Yo tengo ${result} tortas`); // No se ejecuta
})
.catch((error) => {
console.log(error); // En consola: Error: Estoy triste
})
.finally(() => {
console.log("Fiesta"); // Aparece en la consola no importa qué: Fiesta
});
Muy bien, a estas alturas, espero que tengas la idea básica de Promise
. Pasemos a la tarea 2.
Tarea 2: construye un juego de adivinanzas
Los requerimientos:
- Historia de usuario: Un usuario puede introducir un número.
- Historia de usuario: El sistema elige un número aleatorio del 1 al 6.
- Historia de usuario: Si el número de usuario es igual al número aleatorio, le da al usuario 2 puntos.
- Historia de usuario: Si el número del usuario es diferente al número aleatorio por 1, le da al usuario 1 punto. De otra manera, le da al usuario 0 puntos.
- Historia de usuario: El usuario puede jugar tanto como quiera.
Para las primeras 4 historias de usuario, crearemos una función insertaNum
y retorna una Promise
:
const insertaNum = () => {
return new Promise((resolve, reject) => {
// Empecemos desde aquí...
});
};
Lo primero que debemos hacer es pedirle un número al usuario y elegir un número aleatorio entre 1 y 6:
const insertaNum = () => {
return new Promise((resolve, reject) => {
const numUsuario = Number(window.prompt("Introduce un número (1 - 6):"));
// Pide al usuario que introduzca un número
const aleatorio = Math.floor(Math.random() * 6 + 1);
// Elige un número aleatorio del 1 al 6
});
};
Ahora, numUsuario
puede ingresar un valor que no es un número. Si es así, llamemos a la función reject
con un error:
const insertaNum = () => {
return new Promise((resolve, reject) => {
const numUsuario = Number(window.prompt("Introduce un número (1 - 6):"));
// Pide al usuario que introduzca un número
const aleatorio = Math.floor(Math.random() * 6 + 1);
// Elige un número aleatorio del 1 al 6
if (isNaN(numUsuario)) {
reject(new Error("Tipo de entrada incorrecta"));
// Si el usuario introduce un valor que no es un número,
// ejecuta reject con un error
}
});
};
Lo siguiente que queremos hacer es verificar si el numUsuario
es igual a aleatorio
, si es así, queremos darle al usuario 2 puntos y podemos ejecutar la función resolve
pasando un objeto {puntos: 2, aleatorio}
. Observa que también queremos saber el número en aleatorio
cuando se resuelva la promesa.
Si él numUsuario
es diferente de aleatorio
por uno, entonces le damos al usuario 1 punto. De lo contrario, le damos al usuario 0 puntos:
return new Promise((resolve, reject) => {
const numUsuario = Number(window.prompt("Introduce un número (1 - 6):"));
// Pide al usuario que introduzca un número
const aleatorio = Math.floor(Math.random() * 6 + 1);
// Elige un número aleatorio del 1 al 6
if (isNaN(numUsuario)) {
reject(new Error("Tipo de entrada incorrecta"));
// Si el usuario introduce un valor que no es un número,
// ejecuta reject con un error
}
if (numUsuario === aleatorio) {
// Si el número del usuario coincide con el número aleatorio,
// retorna 2 puntos
resolve({
puntos: 2,
aleatorio,
});
} else if (numUsuario === aleatorio - 1 || numUsuario === aleatorio + 1) {
// Si el número del usuario es diferente al número aleatorio por 1,
// retorna 1 punto
resolve({
puntos: 1,
aleatorio,
});
} else {
// Si no, retorna 0 puntos
resolve({
puntos: 0,
aleatorio,
});
}
});
Muy bien, también creemos otra función para preguntar si el usuario quiere continuar el juego:
const continuarJuego = () => {
return new Promise((resolve) => {
if (window.confirm("¿Quieres continuar?")) {
// Pregunta si el usuario quiere continuar el juego
// con un modal de confirmación
resolve(true);
} else {
resolve(false);
}
});
};
Observa que creamos una Promise
, pero no utiliza la función callback reject
. Esto está totalmente bien.
Ahora creemos una función para manejar la suposición:
const suponer = () => {
insertaNum() // Esto retorna una Promesa
.then((result) => {
alert(`Dado: ${result.aleatorio}: obtuviste ${result.puntos} puntos`);
// Cuando resolve se ejecuta, obtenemos los puntos
// y el número aleatorio
// Vamos a preguntarle al usuario si quiere continuar el juego
continuarJuego()
.then((result) => {
if (result) {
suponer(); // Si sí, ejecutamos suponer() de nuevo
} else {
alert("Terminó el juego"); // Si no, mostramos una alerta
}
});
})
.catch((error) => alert(error));
};
suponer(); // Ejecuta la función suponer.
Aquí cuando llamamos suponer()
, insertaNum()
ahora retorna una Promise
:
- Si la
Promise
es resuelta, llamamos al métodothen
y mostramos un mensaje de alerta. También preguntamos si el usuario quiere continuar. - Si la
Promise
es rechazada, mostramos un mensaje de alerta con el error.
Como puedes ver, el código es algo difícil de leer.
Refactoricemos la función suponer
un poco, utilizando la sintáxis async/await
:
const suponer = async () => {
try {
const result = await insertaNum();
// En lugar del método 'then', podemos obtener el resultado
// directamente, poniendo 'await' antes de la promesa
alert(`Dado: ${result.aleatorio}: obtuviste ${result.puntos} puntos`);
const estaContinuando = await continuarJuego();
if (estaContinuando) {
suponer();
} else {
alert("Terminó el juego");
}
} catch (error) {
// En lugar del método 'catch', podemos usar la sintáxis 'try/catch'
alert(error);
}
};
Puedes ver que creamos una función async
, colocando async
antes de las llaves. Entonces en la función async
:
- En lugar del método
then
, podemos obtener los resultados directamente , poniendoawait
antes de la promesa. - En lugar del método
catch
, podemos utilizar la sintáxistry/catch
.
Aquí está todo el código para esta tarea, de nuevo, para tu referencia:
return new Promise((resolve, reject) => {
const numUsuario = Number(window.prompt("Introduce un número (1 - 6):"));
// Pide al usuario que introduzca un número
const aleatorio = Math.floor(Math.random() * 6 + 1);
// Elige un número aleatorio del 1 al 6
if (isNaN(numUsuario)) {
reject(new Error("Tipo de entrada incorrecta"));
// Si el usuario introduce un valor que no es un número,
// ejecuta reject con un error
}
if (numUsuario === aleatorio) {
// Si el número del usuario coincide con el número aleatorio,
// devuelve 2 puntos
resolve({
puntos: 2,
aleatorio,
});
} else if (numUsuario === aleatorio - 1 || numUsuario === aleatorio + 1) {
// Si el número del usuario es diferente al número aleatorio por 1,
// devuelve 1 punto
resolve({
puntos: 1,
aleatorio,
});
} else {
// Si no, devuelve 0 puntos
resolve({
puntos: 0,
aleatorio,
});
}
});
const continuarJuego = () => {
return new Promise((resolve) => {
if (window.confirm("¿Quieres continuar?")) {
// Pregunta si el usuario quiere continuar el juego
// con un modal de confirmación
resolve(true);
} else {
resolve(false);
}
});
};
const suponer = async () => {
try {
const result = await insertaNum();
// En lugar del método 'then', podemos obtener el resultado
// directamente, poniendo 'await' antes de la promesa
alert(`Dado: ${result.aleatorio}: obtuviste ${result.puntos} puntos`);
const estaContinuando = await continuarJuego();
if (estaContinuando) {
suponer();
} else {
alert("Terminó el juego");
}
} catch (error) {
// En lugar del método 'catch', podemos usar la sintaxis 'try/catch'
alert(error);
}
};
Muy bien, hemos terminado con la segunda tarea. Pasemos a la tercera.
Tarea 3: buscar información de un país desde una API
Verás que las Promise
se usan mucho al obtener datos de una API.
Si abres https://restcountries.eu/rest/v2/alpha/col en una nueva ventana, verás los datos del país en formato JSON.
Utilizando el Fetch API, podemos buscar los datos con:
const buscarDatos = async () => {
const res = await fetch("https://restcountries.eu/rest/v2/alpha/col");
// fetch() retorna una promesa, así que necesitamos esperar por ella
const pais = await res.json();
// res es ahora una respuesta HTTP, por lo que
//necesitamos llamar a res.json()
console.log(pais); // Los datos de Colombia se registrarán en la consola
};
buscarDatos();
Ahora que tenemos los datos del país que queremos, pasemos a la última tarea.
Tarea 4: buscar países vecinos de un país
Si abres la tarea 4, verás que tenemos una función buscarPais
, que obtiene los datos del endpoint: https://restcountries.eu/rest/v2/alpha/${alpha3Code}
donde alpha3code
es el código del país .
También verás que detectará cualquier error
que pueda ocurrir al obtener los datos.
// Tarea 4: obtener los países vecinos de Colombia
const buscarPais = async (alpha3Code) => {
try {
const res = await fetch(
`https://restcountries.eu/rest/v2/alpha/${alpha3Code}`
);
const data = await res.json();
return data;
} catch (error) {
console.log(error);
}
};
Creemos una función buscarPaisYVecinos
y obtengamos la información de Colombia pasando col
como alpha3code
.
const buscarPaisYVecinos = async () => {
const colombia = await buscarPais("col");
console.log(colombia);
};
buscarPaisYVecinos();
Ahora, si miras en tu consola, puedes ver un objeto con este aspecto:
En el objeto, hay una propiedad de border
que es una lista de alpha3codes
para los países vecinos de Colombia.
Ahora bien, si intentamos llegar a los países vecinos mediante:
const vecinos = colombia.borders.map((border) => buscarPais(border));
Entonces, vecinos
serán un arreglo de objetos Promise
.
Cuando trabajamos con un arreglo de promesas, necesitamos usar Promise.all
:
const buscarPaisYVecinos = async () => {
const colombia = await buscarPais("col");
const vecinos = await Promise.all(
colombia.borders.map((border) => buscarPais(border))
);
console.log(vecinos);
};
buscarPaisYVecinos();
En la consola
, deberíamos poder ver la lista de objetos de países.
Aquí está todo el código para la tarea 4 nuevamente para tu referencia:
const buscarPais = async (alpha3Code) => {
try {
const res = await fetch(
`https://restcountries.eu/rest/v2/alpha/${alpha3Code}`
);
const data = await res.json();
return data;
} catch (error) {
console.log(error);
}
};
const buscarPaisYVecinos = async () => {
const colombia = await buscarPais("col");
const vecinos = await Promise.all(
colombia.borders.map((border) => buscarPais(border))
);
console.log(vecinos);
};
buscarPaisYVecinos();
Conclusión
Después de completar estas 4 tareas, puedes ver que Promise
es útil cuando se trata de acciones asíncronas, o cosas que no suceden al mismo tiempo.
Puedes ver esto en la práctica en uno de estos tutoriales, donde creamos una aplicación desde cero con React y Next.js:
_________ ? Acerca de Thu Nghiem_________
- Fundador de DevChallenges
- Suscríbete a su canal de YouTube
- Síguelo en Twitter
- Únete a Discord
Traducido del artículo de Thu Nghiem - How to Learn JavaScript Promises and Async/Await in 20 Minutes