Promesas Paralelas y Promise.all
Obtén múltiples fuentes de datos en paralelo para eliminar cascadas y acelera la renderización del servidor.
Busca en todas las páginas de la documentación
Obtén múltiples fuentes de datos en paralelo para eliminar cascadas y acelera la renderización del servidor.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
// 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:
Promise.all toma un array de promesas y devuelve una única promesa que se resuelve en un array de resultadosPromise.all se rechaza inmediatamente con ese errorfetch de Next.js en Server Components se desduplica y cachea automáticamente| 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 |
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[]
);// 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);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.allSettled cuando 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. await solo 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 usa Promise.allSettled.
En realidad no es paralelo. Si accidentalmente await cada 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()]);p-limit o agrupa solicitudes.| 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 |
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:
take: limit + 1 evita una consulta de conteo separada solo para saber si existe una página siguiente. Si obtienes más filas que limit, hay más páginas. Corta la fila adicional antes de devolver.$queryRaw se usa para la agregación específica de Postgres COALESCE(SUM(...)) porque la API de agregación de Prisma no admite COALESCE. El tipo de resultado debe anotarse explícitamente como [{ total: bigint }] porque Postgres devuelve bigint para SUM.bigint de Postgres no puede serializarse a JSON directamente. Number(storageUsed[0]?.total ?? 0n) lo convierte a un número regular. Para valores que podrían exceder Number.MAX_SAFE_INTEGER, usa .toString() en su lugar.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.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.Promise.all se rechaza inmediatamente con ese errorPromise.allSettled cuando algunas obtenciones no sean críticasstatus de "fulfilled" o "rejected" para que puedas manejar cada uno individualmenteawait bloquea la siguiente líneaconst a = await fetchA(); const b = await fetchB(); espera a que A termine antes de crear Bawait Promise.all([...])// Malo: secuencial
const users = await getUsers();
const posts = await getPosts();
// Bueno: paralelo
const [users, posts] = await Promise.all([getUsers(), getPosts()]);Promise.race se resuelve o se rechaza con la primera promesa que se resuelve (cumplimiento o rechazo)Promise.any se resuelve con la primera promesa que se cumple, ignorando rechazosPromise.any solo se rechaza si todas las promesas se rechazan (con un AggregateError)const results = await Promise.all([
getUser("1"), // Promise<User>
getPosts("1"), // Promise<Post[]>
getCount(), // Promise<number>
]);
// results es [User, Post[], number] -- completamente tipadofunction 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);Promise.all para obtenciones sub-independientesconst user = await getUser(id);
const [posts, followers] = await Promise.all([
getPostsByAuthor(user.id),
getFollowers(user.id),
]);num_cpus * 2 + 1)p-limit para agrupar solicitudeslimit, hay más páginas (hasMore = true)COUNT(*) separada solo para verificar la página siguiente<Suspense> obtiene de forma independientePromise.all, las secciones se renderiz an mientras se resuelven en lugar de esperar a que todo terminePromise.all cuando necesites todos los datos antes de renderizarbigint para agregaciones SUMbigint no puede serializarse a JSON directamente (lanza TypeError)Number() lo convierte a un número regular; usa .toString() si el valor podría exceder Number.MAX_SAFE_INTEGERasync function fetchParallel<T extends readonly Promise<unknown>[]>(
...promises: T
): Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }> {
return Promise.all(promises) as any;
}Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥