Blog

RA

Rod Alexanderson

Desarrollador Web

Creando documentación técnica en español para desarrolladores de Latinoamérica.

Más sobre mí →

Suscríbete al Newsletter

Recibe los nuevos artículos directamente en tu email.

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:

  1. Obtener información del usuario
  2. Luego obtener sus posts
  3. 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:

  1. async marca una función como asíncrona
  2. await pausa la ejecución hasta que una Promise se resuelva
  3. Solo puedes usar await dentro de funciones async
  4. Siempre maneja errores con try/catch
  5. Usa Promise.all para operaciones en paralelo
  6. Es más legible que callbacks o .then()
  7. Funciona con cualquier cosa que retorne una Promise

Tabla de referencia rápida:

SituaciónCódigo
Función asíncrona básicaasync function nombre() { }
Esperar una Promiseconst resultado = await promesa
Manejo de errorestry { await... } catch(e) { }
Múltiples peticiones en paraleloawait Promise.all([p1, p2, p3])
Múltiples peticiones en secuenciaawait p1; await p2; await p3
Función que retorna valorasync 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.