Async/Await en JavaScript: La Guía Definitiva
Publicado el 1 de octubre, 2025 • 15 min de lectura
Si estás aprendiendo JavaScript, probablemente has visto código con async
y await
y te has preguntado qué significan. Esta guía te explicará todo desde cero, sin asumir conocimientos previos.
El problema que async/await resuelve
Imagina que estás haciendo una receta de cocina:
Receta síncrona (paso a paso, esperando cada cosa):
1. Hervir agua (esperas 5 minutos)
2. Cuando hierva, añadir pasta (esperas 10 minutos)
3. Cuando esté lista, escurrir
4. Servir
Cada paso bloquea al siguiente. No puedes hacer nada más mientras esperas que hierva el agua.
Receta asíncrona (haciendo varias cosas a la vez):
1. Poner agua a hervir (NO esperas, sigues con otras cosas)
2. Mientras tanto, cortar verduras
3. Mientras tanto, preparar la salsa
4. Cuando el agua hierva (te avisa), añadir pasta
5. Cuando esté lista (te avisa), escurrir
6. Servir
JavaScript funciona igual. Algunas operaciones tardan tiempo:
- Obtener datos de una API (puede tardar 2 segundos)
- Leer un archivo (puede tardar 1 segundo)
- Consultar una base de datos (puede tardar 3 segundos)
No queremos que tu aplicación se "congele" esperando. Ahí entra async/await.
¿Qué significa código asíncrono?
Código síncrono = Una cosa después de otra, en orden, esperando cada una
console.log('1. Inicio')
console.log('2. Medio')
console.log('3. Final')
// Resultado:
// 1. Inicio
// 2. Medio
// 3. Final
Todo se ejecuta en orden, una línea tras otra.
Código asíncrono = Iniciar una operación y seguir con otras cosas mientras tanto
console.log('1. Inicio')
setTimeout(() => {
console.log('2. Medio (después de 2 segundos)')
}, 2000)
console.log('3. Final')
// Resultado:
// 1. Inicio
// 3. Final
// 2. Medio (después de 2 segundos)
JavaScript no espera a setTimeout
. Sigue ejecutando y cuando pasan 2 segundos, ejecuta lo que estaba dentro.
JavaScript es single-threaded
JavaScript ejecuta una cosa a la vez (single-threaded), pero puede "registrar" tareas para hacer después mientras hace otras cosas. Es como un chef que pone algo al horno, y mientras se cocina, prepara otra cosa.
Antes de async/await: Callbacks
Antes de async/await, usábamos callbacks (funciones que se ejecutan cuando algo termina):
// Forma antigua con callbacks
function obtenerUsuario(id, callback) {
setTimeout(() => {
const usuario = { id: id, nombre: 'Ana' }
callback(usuario)
}, 1000)
}
obtenerUsuario(1, (usuario) => {
console.log('Usuario obtenido:', usuario)
})
Esto funciona, pero se vuelve un desastre con múltiples operaciones:
// Callback hell (infierno de callbacks)
obtenerUsuario(1, (usuario) => {
obtenerPosts(usuario.id, (posts) => {
obtenerComentarios(posts[0].id, (comentarios) => {
obtenerLikes(comentarios[0].id, (likes) => {
console.log('Finalmente tengo los likes')
})
})
})
})
Esto es difícil de leer, difícil de mantener, y propenso a errores.
Promises: El paso intermedio
Luego llegaron las Promises (promesas):
function obtenerUsuario(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const usuario = { id: id, nombre: 'Ana' }
resolve(usuario) // Éxito
}, 1000)
})
}
obtenerUsuario(1)
.then(usuario => {
console.log('Usuario obtenido:', usuario)
})
.catch(error => {
console.error('Error:', error)
})
Mejor que callbacks, pero todavía algo confuso.
Async/Await: La solución moderna
Async/await hace que el código asíncrono se vea y se comporte como código síncrono:
async function obtenerDatos() {
const usuario = await obtenerUsuario(1)
console.log('Usuario obtenido:', usuario)
}
obtenerDatos()
Mucho más claro y fácil de leer.
Sintaxis de async/await
La palabra clave async
async
se pone antes de una función para decir "esta función trabaja con código asíncrono":
// Función normal
function saludar() {
return 'Hola'
}
// Función asíncrona
async function saludarAsync() {
return 'Hola'
}
Cuando marcas una función como async
, automáticamente retorna una Promise:
async function obtenerNumero() {
return 42
}
// Es equivalente a:
function obtenerNumero() {
return Promise.resolve(42)
}
// Usar la función
obtenerNumero().then(numero => {
console.log(numero) // 42
})
La palabra clave await
await
solo funciona dentro de funciones async
. Le dice a JavaScript "espera a que esto termine":
async function obtenerDatos() {
// Espera a que obtenerUsuario termine
const usuario = await obtenerUsuario(1)
// Esta línea no se ejecuta hasta que usuario esté listo
console.log(usuario)
}
Regla importante: Solo puedes usar await
dentro de funciones marcadas con async
.
// ❌ Error: await solo funciona en funciones async
function miFuncion() {
const datos = await fetch('url') // Error
}
// ✓ Correcto
async function miFuncion() {
const datos = await fetch('url') // Funciona
}
Ejemplos prácticos
Ejemplo 1: Obtener datos de una API
Sin async/await (con Promises):
function obtenerUsuarios() {
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(usuarios => {
console.log(usuarios)
})
.catch(error => {
console.error('Error:', error)
})
}
Con async/await:
async function obtenerUsuarios() {
const response = await fetch('https://jsonplaceholder.typicode.com/users')
const usuarios = await response.json()
console.log(usuarios)
}
obtenerUsuarios()
Mucho más limpio y fácil de leer.
Ejemplo 2: Múltiples peticiones en secuencia
Imagina que necesitas:
- Obtener información del usuario
- Luego obtener sus posts
- Luego obtener comentarios del primer post
async function obtenerDatosCompletos(userId) {
// Paso 1: Obtener usuario
const responseUsuario = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
const usuario = await responseUsuario.json()
console.log('1. Usuario obtenido:', usuario.name)
// Paso 2: Obtener posts del usuario
const responsePosts = await fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`)
const posts = await responsePosts.json()
console.log('2. Posts obtenidos:', posts.length)
// Paso 3: Obtener comentarios del primer post
const responseComentarios = await fetch(`https://jsonplaceholder.typicode.com/comments?postId=${posts[0].id}`)
const comentarios = await responseComentarios.json()
console.log('3. Comentarios obtenidos:', comentarios.length)
return {
usuario,
posts,
comentarios
}
}
obtenerDatosCompletos(1)
Cada await
espera a que la operación anterior termine antes de continuar.
Ejemplo 3: Múltiples peticiones en paralelo
Si las operaciones NO dependen una de otra, puedes ejecutarlas en paralelo:
async function obtenerTodosLosDatos() {
// Iniciar todas las peticiones al mismo tiempo
const promiseUsuarios = fetch('https://jsonplaceholder.typicode.com/users')
const promisePosts = fetch('https://jsonplaceholder.typicode.com/posts')
const promiseComentarios = fetch('https://jsonplaceholder.typicode.com/comments')
// Esperar a que TODAS terminen
const [responseUsuarios, responsePosts, responseComentarios] = await Promise.all([
promiseUsuarios,
promisePosts,
promiseComentarios
])
// Convertir todas a JSON
const usuarios = await responseUsuarios.json()
const posts = await responsePosts.json()
const comentarios = await responseComentarios.json()
console.log('Usuarios:', usuarios.length)
console.log('Posts:', posts.length)
console.log('Comentarios:', comentarios.length)
}
obtenerTodosLosDatos()
Promise.all
ejecuta todas las promesas en paralelo. Es mucho más rápido que hacerlas una por una.
Comparación de tiempos:
// En secuencia (lento)
// Si cada petición tarda 1 segundo = 3 segundos total
const usuarios = await fetch('url1')
const posts = await fetch('url2')
const comentarios = await fetch('url3')
// En paralelo (rápido)
// Si cada petición tarda 1 segundo = 1 segundo total (todas al mismo tiempo)
const [usuarios, posts, comentarios] = await Promise.all([
fetch('url1'),
fetch('url2'),
fetch('url3')
])
Ejemplo 4: Función que retorna datos
async function obtenerProductos() {
const response = await fetch('https://api.ejemplo.com/productos')
const productos = await response.json()
// Retornar los productos
return productos
}
// Usar la función
async function mostrarProductos() {
const productos = await obtenerProductos()
productos.forEach(producto => {
console.log(`${producto.nombre}: $${producto.precio}`)
})
}
mostrarProductos()
Manejo de errores con try/catch
Cuando trabajas con código asíncrono, las cosas pueden fallar. Usa try/catch
para manejar errores:
Sintaxis básica
async function obtenerDatos() {
try {
// Código que puede fallar
const response = await fetch('https://api.ejemplo.com/datos')
const datos = await response.json()
console.log(datos)
} catch (error) {
// Si algo falla, se ejecuta esto
console.error('Algo salió mal:', error)
}
}
Ejemplo completo con manejo de errores
async function obtenerUsuario(id) {
try {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
// Verificar si la petición fue exitosa
if (!response.ok) {
throw new Error(`Error HTTP: ${response.status}`)
}
const usuario = await response.json()
return usuario
} catch (error) {
console.error('Error al obtener usuario:', error.message)
// Retornar un valor por defecto o lanzar el error de nuevo
return null
}
}
// Usar la función
async function main() {
const usuario = await obtenerUsuario(1)
if (usuario) {
console.log('Usuario encontrado:', usuario.name)
} else {
console.log('No se pudo obtener el usuario')
}
}
main()
Múltiples bloques try/catch
Puedes tener múltiples bloques para manejar diferentes tipos de errores:
async function procesarDatos() {
let usuario = null
let posts = null
// Intentar obtener usuario
try {
const response = await fetch('https://api.ejemplo.com/usuario/1')
usuario = await response.json()
} catch (error) {
console.error('Error obteniendo usuario:', error)
// Continúa ejecutando aunque falle
}
// Intentar obtener posts
try {
const response = await fetch('https://api.ejemplo.com/posts')
posts = await response.json()
} catch (error) {
console.error('Error obteniendo posts:', error)
}
// Mostrar lo que se pudo obtener
if (usuario) console.log('Usuario:', usuario.name)
if (posts) console.log('Posts:', posts.length)
}
Patrones comunes
Patrón 1: Función auxiliar para fetch
Crea una función reutilizable para hacer peticiones:
async function fetchJSON(url) {
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`)
}
return await response.json()
} catch (error) {
console.error('Error en fetch:', error)
throw error // Re-lanzar para que quien llame la función pueda manejarlo
}
}
// Usar la función auxiliar
async function obtenerDatos() {
try {
const usuarios = await fetchJSON('https://jsonplaceholder.typicode.com/users')
const posts = await fetchJSON('https://jsonplaceholder.typicode.com/posts')
console.log('Usuarios:', usuarios.length)
console.log('Posts:', posts.length)
} catch (error) {
console.error('Error obteniendo datos:', error)
}
}
obtenerDatos()
Patrón 2: Timeout (límite de tiempo)
A veces quieres que una operación falle si tarda mucho:
async function fetchConTimeout(url, tiempoLimite = 5000) {
// Crear una promesa que se rechaza después del tiempo límite
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), tiempoLimite)
})
// Hacer la petición
const peticion = fetch(url)
// Retornar la que termine primero
return Promise.race([peticion, timeout])
}
async function obtenerDatos() {
try {
// Si tarda más de 3 segundos, falla
const response = await fetchConTimeout('https://api.ejemplo.com/datos', 3000)
const datos = await response.json()
console.log(datos)
} catch (error) {
if (error.message === 'Timeout') {
console.error('La petición tardó demasiado')
} else {
console.error('Error:', error)
}
}
}
Patrón 3: Reintentos automáticos
Reintentar una operación si falla:
async function fetchConReintentos(url, intentosMaximos = 3) {
for (let intento = 1; intento <= intentosMaximos; intento++) {
try {
console.log(`Intento ${intento} de ${intentosMaximos}`)
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`)
}
return await response.json()
} catch (error) {
// Si es el último intento, lanzar el error
if (intento === intentosMaximos) {
throw error
}
// Esperar un poco antes de reintentar
await new Promise(resolve => setTimeout(resolve, 1000))
}
}
}
async function obtenerDatos() {
try {
const datos = await fetchConReintentos('https://api.ejemplo.com/datos', 3)
console.log('Datos obtenidos:', datos)
} catch (error) {
console.error('Falló después de 3 intentos:', error)
}
}
Patrón 4: Estados de carga en React
Común en aplicaciones React:
function ProductosComponent() {
const [productos, setProductos] = useState([])
const [cargando, setCargando] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
async function cargarProductos() {
try {
setCargando(true)
const response = await fetch('https://api.ejemplo.com/productos')
const datos = await response.json()
setProductos(datos)
} catch (err) {
setError(err.message)
} finally {
setCargando(false)
}
}
cargarProductos()
}, [])
if (cargando) return <div>Cargando...</div>
if (error) return <div>Error: {error}</div>
return (
<div>
{productos.map(producto => (
<div key={producto.id}>{producto.nombre}</div>
))}
</div>
)
}
Errores comunes
Error 1: Olvidar await
// ❌ Incorrecto
async function obtenerDatos() {
const response = fetch('url') // Falta await
const datos = response.json() // Error: response es una Promise, no tiene .json()
console.log(datos)
}
// ✓ Correcto
async function obtenerDatos() {
const response = await fetch('url')
const datos = await response.json()
console.log(datos)
}
Error 2: Usar await fuera de función async
// ❌ Incorrecto
function obtenerDatos() {
const datos = await fetch('url') // Error: await solo funciona en funciones async
}
// ✓ Correcto
async function obtenerDatos() {
const datos = await fetch('url')
}
Error 3: No manejar errores
// ❌ Peligroso: si falla, la app puede romperse
async function obtenerDatos() {
const response = await fetch('url')
const datos = await response.json()
return datos
}
// ✓ Correcto: siempre maneja errores
async function obtenerDatos() {
try {
const response = await fetch('url')
const datos = await response.json()
return datos
} catch (error) {
console.error('Error:', error)
return null
}
}
Error 4: Await en un loop (puede ser lento)
// ❌ Lento: hace una petición, espera, hace otra, espera...
async function obtenerUsuarios(ids) {
const usuarios = []
for (const id of ids) {
const usuario = await fetch(`/usuarios/${id}`)
usuarios.push(await usuario.json())
}
return usuarios
}
// ✓ Rápido: hace todas las peticiones en paralelo
async function obtenerUsuarios(ids) {
const promesas = ids.map(id =>
fetch(`/usuarios/${id}`).then(r => r.json())
)
return await Promise.all(promesas)
}
Error 5: No retornar await en funciones
// ❌ Incorrecto: no retornas la promesa correctamente
async function obtenerDatos() {
await fetch('url') // No retornas nada
}
// ✓ Correcto: retornas el valor
async function obtenerDatos() {
const response = await fetch('url')
const datos = await response.json()
return datos // Retornar los datos
}
Async/await en diferentes contextos
En Node.js
// Leer archivos de forma asíncrona
const fs = require('fs').promises
async function leerArchivo() {
try {
const contenido = await fs.readFile('archivo.txt', 'utf8')
console.log(contenido)
} catch (error) {
console.error('Error leyendo archivo:', error)
}
}
leerArchivo()
En NextJS (Server Components)
// app/productos/page.tsx
export default async function ProductosPage() {
// Obtener datos directamente en el servidor
const productos = await fetch('https://api.ejemplo.com/productos')
.then(r => r.json())
return (
<div>
{productos.map(producto => (
<div key={producto.id}>{producto.nombre}</div>
))}
</div>
)
}
En Event Handlers
// En React
function BotonGuardar() {
const handleClick = async () => {
try {
const response = await fetch('/api/guardar', {
method: 'POST',
body: JSON.stringify({ datos: 'ejemplo' })
})
if (response.ok) {
alert('Guardado exitoso')
}
} catch (error) {
alert('Error al guardar')
}
}
return <button onClick={handleClick}>Guardar</button>
}
Cuándo usar async/await
Usa async/await cuando:
- Obtienes datos de APIs
- Lees/escribes archivos
- Consultas bases de datos
- Usas cualquier función que retorne una Promise
- Quieres código más limpio y legible
No necesitas async/await cuando:
- Todo tu código es síncrono (no hay operaciones que tarden tiempo)
- Trabajas solo con valores inmediatos
- No estás usando Promises
Comparación: Callbacks vs Promises vs Async/Await
El mismo código escrito de tres formas:
// 1. Con Callbacks (difícil de leer)
function obtenerDatos(callback) {
obtenerUsuario(1, (usuario) => {
obtenerPosts(usuario.id, (posts) => {
obtenerComentarios(posts[0].id, (comentarios) => {
callback(comentarios)
})
})
})
}
// 2. Con Promises (mejor pero verboso)
function obtenerDatos() {
return obtenerUsuario(1)
.then(usuario => obtenerPosts(usuario.id))
.then(posts => obtenerComentarios(posts[0].id))
.then(comentarios => comentarios)
}
// 3. Con Async/Await (más claro)
async function obtenerDatos() {
const usuario = await obtenerUsuario(1)
const posts = await obtenerPosts(usuario.id)
const comentarios = await obtenerComentarios(posts[0].id)
return comentarios
}
Async/await es claramente más fácil de leer y mantener.
Recursos y práctica
Para practicar async/await, prueba estos ejercicios:
Ejercicio 1: Básico
// Crea una función que obtenga datos de esta API
// https://jsonplaceholder.typicode.com/users
// Y muestre el nombre del primer usuario
async function ejercicio1() {
// Tu código aquí
}
Ejercicio 2: Intermedio
// Obtén los primeros 5 usuarios y para cada uno,
// obtén sus posts. Muestra cuántos posts tiene cada usuario.
async function ejercicio2() {
// Tu código aquí
}
Ejercicio 3: Avanzado
// Crea una función que:
// 1. Obtenga un usuario aleatorio
// 2. Obtenga todos sus posts
// 3. Para cada post, obtenga los comentarios
// 4. Retorne el post con más comentarios
// Todo con manejo de errores apropiado
async function ejercicio3() {
// Tu código aquí
}
Resumen
Puntos clave sobre async/await:
async
marca una función como asíncronaawait
pausa la ejecución hasta que una Promise se resuelva- Solo puedes usar
await
dentro de funcionesasync
- Siempre maneja errores con try/catch
- Usa
Promise.all
para operaciones en paralelo - Es más legible que callbacks o .then()
- Funciona con cualquier cosa que retorne una Promise
Tabla de referencia rápida:
Situación | Código |
---|---|
Función asíncrona básica | async function nombre() { } |
Esperar una Promise | const resultado = await promesa |
Manejo de errores | try { await... } catch(e) { } |
Múltiples peticiones en paralelo | await Promise.all([p1, p2, p3]) |
Múltiples peticiones en secuencia | await p1; await p2; await p3 |
Función que retorna valor | async function() { return await promesa } |
Async/await hace que trabajar con código asíncrono sea mucho más simple. Con práctica, se volverá segunda naturaleza.
Recursos adicionales:
¿Tienes dudas sobre async/await? Déjame un comentario abajo.
Sobre el autor: Desarrollador especializado en JavaScript, React y NextJS. Creo contenido educativo en español para ayudar a la comunidad de desarrolladores.