Promesas Paralelas y Promise.all
Obtén múltiples fuentes de datos en paralelo para eliminar cascadas y acelera la renderización del servidor.
Receta
Referencia rápida para patrones de obtención de datos en paralelo.
// Promise.all: falla rápidamente si alguna promesa se rechaza
const [users, posts, stats] = await Promise.all([
getUsers(),
getPosts(),
getStats(),
]);
// Promise.allSettled: nunca se rechaza, devuelve el estado para cada una
const results = await Promise.allSettled([
getUsers(),
getPosts(),
getStats(),
]);
// Promise.race: se resuelve/rechaza con la primera que se resuelve
const fastest = await Promise.race([
fetchFromPrimary(),
fetchFromFallback(),
]);
// Promise.any: se resuelve con la primera que se cumple (ignora rechazos)
const firstSuccess = await Promise.any([
fetchFromCDN1(),
fetchFromCDN2(),
]);Cuándo usarlo: Tienes múltiples obtenciones de datos independientes en un Server Component y deseas que se ejecuten simultáneamente en lugar de secuencialmente.
Ejemplo Funcionando
// app/dashboard/page.tsx
import { Suspense } from "react";
async function getUser(id: string) {
const res = await fetch(`https://api.example.com/users/${id}`);
return res.json();
}
async function getOrders(userId: string) {
const res = await fetch(`https://api.example.com/orders?userId=${userId}`);
return res.json();
}
async function getNotifications(userId: string) {
const res = await fetch(`https://api.example.com/notifications?userId=${userId}`);
return res.json();
}
export default async function DashboardPage() {
const userId = "user-123";
// Las tres obtenciones comienzan al mismo tiempo
const [user, orders, notifications] = await Promise.all([
getUser(userId),
getOrders(userId),
getNotifications(userId),
]);
return (
<div>
<h1>Bienvenido, {user.name}</h1>
<p>{notifications.length} notificaciones sin leer</p>
<h2>Pedidos Recientes</h2>
<ul>
{orders.map((order: { id: string; total: number }) => (
<li key={order.id}>${order.total}</li>
))}
</ul>
</div>
);
}Lo que esto demuestra:
- Tres llamadas independientes a la API se ejecutan en paralelo
- El tiempo total es igual a la llamada más lenta (no la suma de los tres)
- Desestructuración limpia de resultados
- Server Component sin JavaScript en el cliente
Profundización
Cómo Funciona
Promise.alltoma un array de promesas y devuelve una única promesa que se resuelve en un array de resultados- Todas las promesas comienzan a ejecutarse inmediatamente cuando se crean (no cuando se esperan)
- La promesa devuelta se resuelve cuando TODAS las promesas se cumplen
- Si ALGUNA promesa se rechaza,
Promise.allse rechaza inmediatamente con ese error fetchde Next.js en Server Components se desduplica y cachea automáticamente
Parámetros y Valores Devueltos
| Método | Se Resuelve Cuando | Se Rechaza Cuando | Devuelve |
|---|---|---|---|
Promise.all | Todas se cumplen | Alguna se rechaza | Array de valores |
Promise.allSettled | Todas se resuelven | Nunca | Array de {status, value} o {status, reason} |
Promise.race | La primera se resuelve | La primera se rechaza | Un único valor |
Promise.any | La primera se cumple | Todas se rechazan | Un único valor |
Variaciones
Promise.allSettled para fallos parciales:
export default async function DashboardPage() {
const results = await Promise.allSettled([
getUser("user-123"),
getOrders("user-123"),
getRecommendations("user-123"), // Podría fallar, no crítico
]);
const user = results[0].status === "fulfilled" ? results[0].value : null;
const orders = results[1].status === "fulfilled" ? results[1].value : [];
const recs = results[2].status === "fulfilled" ? results[2].value : [];
return (
<div>
{user && <h1>{user.name}</h1>}
<OrderList orders={orders} />
{recs.length > 0 && <Recommendations items={recs} />}
</div>
);
}Obtenciones paralelas con Suspense (streaming independiente):
// Cada sección se carga independientemente - no se necesita Promise.all
export default function DashboardPage() {
return (
<div>
<Suspense fallback={<Skeleton />}>
<UserProfile id="user-123" />
</Suspense>
<Suspense fallback={<Skeleton />}>
<OrderList userId="user-123" />
</Suspense>
<Suspense fallback={<Skeleton />}>
<Notifications userId="user-123" />
</Suspense>
</div>
);
}
// Cada componente obtiene sus propios datos
async function UserProfile({ id }: { id: string }) {
const user = await getUser(id);
return <h1>{user.name}</h1>;
}Obtenciones dependientes (la cascada es correcta):
// Cuando la obtención B depende de la obtención A, lo secuencial es correcto
export default async function UserPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const user = await getUser(id);
// Estas dependen de los datos del usuario, pero son independientes entre sí
const [posts, followers] = await Promise.all([
getPostsByAuthor(user.id),
getFollowers(user.id),
]);
return <Profile user={user} posts={posts} followers={followers} />;
}Función auxiliar para obtenciones paralelas tipadas:
async function fetchParallel<T extends readonly Promise<unknown>[]>(
...promises: T
): Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }> {
return Promise.all(promises) as Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }>;
}
// Uso - resultados completamente tipados
const [user, posts] = await fetchParallel(
getUser("123"), // Tipo User
getPosts("123"), // Tipo Post[]
);Notas de TypeScript
// Promise.all preserva tipos de tupla
const results = await Promise.all([
getUser("1"), // devuelve Promise<User>
getPosts("1"), // devuelve Promise<Post[]>
getCount(), // devuelve Promise<number>
]);
// results es [User, Post[], number]
// Tipo de resultado de Promise.allSettled
type SettledResult<T> =
| { status: "fulfilled"; value: T }
| { status: "rejected"; reason: unknown };
// Type guard para resultados asentados
function isFulfilled<T>(
result: PromiseSettledResult<T>
): result is PromiseFulfilledResult<T> {
return result.status === "fulfilled";
}
const results = await Promise.allSettled([getUsers(), getPosts()]);
const successfulResults = results.filter(isFulfilled).map(r => r.value);Trampas
-
Promise.all falla rápidamente. Si una promesa se rechaza, pierdes TODOS los resultados, incluso de las promesas que tuvieron éxito. Solución: Usa
Promise.allSettledcuando algunas obtenciones no son críticas, o envuelve las promesas individuales en try/catch. -
Las promesas comienzan inmediatamente, no cuando se esperan.
const p = fetch(url)inicia la obtención de inmediato.awaitsolo espera el resultado. Si creas promesas en un bucle, todas comienzan en paralelo. Solución: Esto es generalmente lo que deseas, pero sé consciente de las APIs con límite de velocidad. -
Rechazo no manejado en Promise.all. Si olvidas capturar el rechazo de
Promise.all, se convierte en un rechazo de promesa no manejado y podría bloquear tu servidor. Solución: Siempre envuelve en try/catch o usaPromise.allSettled. -
En realidad no es paralelo. Si accidentalmente
awaitcada obtención antes de iniciar la siguiente, se ejecutan secuencialmente. Solución: Crea todas las promesas primero, luego espéralas juntas.
// Malo: secuencial (cada await bloquea)
const users = await getUsers();
const posts = await getPosts();
// Bueno: paralelo (ambas comienzan inmediatamente)
const [users, posts] = await Promise.all([getUsers(), getPosts()]);- Demasiadas solicitudes paralelas. Iniciar 50 obtenciones en paralelo puede saturar las APIs o alcanzar límites de velocidad. Solución: Usa un limitador de concurrencia como
p-limito agrupa solicitudes.
Alternativas
| Alternativa | Usa Cuando | No Uses Cuando |
|---|---|---|
await secuencial | Las obtenciones dependen entre sí (B necesita el resultado de A) | Obtenciones independientes (crea cascadas) |
| Límites de Suspense | Cada sección debe transmitirse independientemente | Necesitas todos los datos antes de renderizar nada |
Promise.allSettled | Algunas obtenciones son opcionales o pueden fallar | Todos los datos son necesarios para renderizar |
Promise.race | Deseas la respuesta más rápida de múltiples fuentes | Necesitas todos los resultados |
p-limit | Necesitas limitar la concurrencia (APIs con límite de velocidad) | Pocas obtenciones paralelas |
Ejemplo del Mundo Real
De una aplicación Next.js 15 / React 19 SaaS en producción (SystemsArchitect.io).
// Ejemplo de producción: API de documentos de administración con 6 consultas paralelas de Prisma
// Archivo: app/api/admin/documents/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function GET(request: NextRequest) {
const { searchParams } = request.nextUrl;
const page = parseInt(searchParams.get('page') ?? '1', 10);
const limit = parseInt(searchParams.get('limit') ?? '20', 10);
const offset = (page - 1) * limit;
const [
documents,
totalCount,
categoryCounts,
recentlyUpdated,
publishedCount,
storageUsed,
] = await Promise.all([
// Consulta principal con truco de paginación: obtén limit+1 para detectar si hay más páginas
prisma.document.findMany({
take: limit + 1,
skip: offset,
orderBy: { updatedAt: 'desc' },
include: { author: { select: { name: true, email: true } } },
}),
prisma.document.count(),
prisma.document.groupBy({
by: ['categoryId'],
_count: { id: true },
}),
prisma.document.findMany({
take: 5,
orderBy: { updatedAt: 'desc' },
where: { updatedAt: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) } },
}),
prisma.document.count({ where: { status: 'PUBLISHED' } }),
// Consulta bruta específica de Postgres para agregación
prisma.$queryRaw<[{ total: bigint }]>`
SELECT COALESCE(SUM("file_size"), 0) as total FROM "Document"
`,
]);
const hasMore = documents.length > limit;
const paginatedDocs = hasMore ? documents.slice(0, limit) : documents;
const totalStorage = Number(storageUsed[0]?.total ?? 0n);
return NextResponse.json({
documents: paginatedDocs,
hasMore,
totalCount,
categoryCounts,
recentlyUpdated,
publishedCount,
totalStorage,
});
}Lo que esto demuestra en producción:
- El truco de paginación
take: limit + 1evita una consulta de conteo separada solo para saber si existe una página siguiente. Si obtienes más filas quelimit, hay más páginas. Corta la fila adicional antes de devolver. $queryRawse usa para la agregación específica de PostgresCOALESCE(SUM(...))porque la API de agregación de Prisma no admiteCOALESCE. El tipo de resultado debe anotarse explícitamente como[{ total: bigint }]porque Postgres devuelvebigintparaSUM.- El
bigintde Postgres no puede serializarse a JSON directamente.Number(storageUsed[0]?.total ?? 0n)lo convierte a un número regular. Para valores que podrían excederNumber.MAX_SAFE_INTEGER, usa.toString()en su lugar. - Seis consultas paralelas se ejecutan simultáneamente mediante
Promise.all. El tiempo total es la duración de la consulta más lenta, no la suma de los seis. En un arranque en frío esto llevó el endpoint de alrededor de 1200ms (secuencial) a menos de 300ms. - El tamaño del grupo de conexiones importa con consultas paralelas. Seis consultas de Prisma simultáneas consumen seis conexiones del grupo. El tamaño predeterminado del grupo de Prisma es
num_cpus * 2 + 1. En plataformas sin servidor, mantén el conteo de consultas paralelas dentro de los límites del grupo o aumenta el tamaño del grupo en la cadena de conexión.
Preguntas Frecuentes
¿Qué sucede si una promesa se rechaza en Promise.all?
Promise.allse rechaza inmediatamente con ese error- Todos los otros resultados se pierden, incluso de las promesas que ya tuvieron éxito
- Usa
Promise.allSettledcuando algunas obtenciones no sean críticas
¿Cuándo deberías usar Promise.allSettled en lugar de Promise.all?
- Cuando algunas obtenciones son opcionales o no críticas (por ejemplo, recomendaciones)
- Cuando deseas datos parciales en lugar de un fallo completo
- Cada resultado tiene un
statusde"fulfilled"o"rejected"para que puedas manejar cada uno individualmente
¿Por qué las llamadas await secuenciales crean una cascada aunque cada promesa comience inmediatamente?
- Las promesas comienzan a ejecutarse cuando se crean, pero
awaitbloquea la siguiente línea const a = await fetchA(); const b = await fetchB();espera a que A termine antes de crear B- Para ejecutar en paralelo, crea todas las promesas primero, luego
await Promise.all([...])
// Malo: secuencial
const users = await getUsers();
const posts = await getPosts();
// Bueno: paralelo
const [users, posts] = await Promise.all([getUsers(), getPosts()]);¿Cuál es la diferencia entre Promise.race y Promise.any?
Promise.racese resuelve o se rechaza con la primera promesa que se resuelve (cumplimiento o rechazo)Promise.anyse resuelve con la primera promesa que se cumple, ignorando rechazosPromise.anysolo se rechaza si todas las promesas se rechazan (con unAggregateError)
¿Cómo Promise.all preserva los tipos de tupla de TypeScript?
const results = await Promise.all([
getUser("1"), // Promise<User>
getPosts("1"), // Promise<Post[]>
getCount(), // Promise<number>
]);
// results es [User, Post[], number] -- completamente tipado¿Cómo escribes un type guard para resultados de Promise.allSettled?
function isFulfilled<T>(
result: PromiseSettledResult<T>
): result is PromiseFulfilledResult<T> {
return result.status === "fulfilled";
}
const results = await Promise.allSettled([getUsers(), getPosts()]);
const successes = results.filter(isFulfilled).map((r) => r.value);¿Cuándo es realmente correcta una cascada secuencial?
- Cuando la obtención B depende del resultado de la obtención A (por ejemplo, necesitas la ID del usuario antes de obtener sus posts)
- Combina: obtén la dependencia primero, luego usa
Promise.allpara obtenciones sub-independientes
const user = await getUser(id);
const [posts, followers] = await Promise.all([
getPostsByAuthor(user.id),
getFollowers(user.id),
]);¿Qué sucede si inicias demasiadas solicitudes paralelas a la vez?
- Puedes saturar las APIs o alcanzar límites de velocidad
- Los grupos de conexiones de la base de datos pueden agotarse (por ejemplo, el tamaño predeterminado del grupo de Prisma es
num_cpus * 2 + 1) - Usa un limitador de concurrencia como
p-limitpara agrupar solicitudes
¿Cómo funciona el truco de paginación take: limit + 1 del ejemplo del mundo real?
- Obtén una fila adicional más allá de tu tamaño de página
- Si obtienes más filas que
limit, hay más páginas (hasMore = true) - Corta la fila adicional antes de devolver al cliente
- Esto evita una consulta
COUNT(*)separada solo para verificar la página siguiente
¿Puedes usar límites de Suspense en lugar de Promise.all para carga de datos paralela?
- Sí. Cada Server Component asincrónico en su propio límite
<Suspense>obtiene de forma independiente - A diferencia de
Promise.all, las secciones se renderiz an mientras se resuelven en lugar de esperar a que todo termine - Usa Suspense cuando las secciones deben transmitirse independientemente; usa
Promise.allcuando necesites todos los datos antes de renderizar
¿Por qué el ejemplo del mundo real convierte bigint a Number() antes de devolver JSON?
- Postgres devuelve
bigintpara agregacionesSUM bigintno puede serializarse a JSON directamente (lanzaTypeError)Number()lo convierte a un número regular; usa.toString()si el valor podría excederNumber.MAX_SAFE_INTEGER
¿Qué es la función auxiliar fetchParallel y por qué es útil?
async function fetchParallel<T extends readonly Promise<unknown>[]>(
...promises: T
): Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }> {
return Promise.all(promises) as any;
}- Preserva los tipos individuales para cada promesa en la tupla
- Proporciona resultados completamente tipados y desestructurados sin anotaciones de tipo manual
Relacionado
- Obtención de Datos - fetch en Server Components
- Streaming - streaming paralelo basado en Suspense
- Almacenamiento en Caché - deduplicación y almacenamiento en caché de solicitudes
- Generadores - iteración asincrónica para conjuntos de datos grandes