Menu

Async await en TypeScript: tipos, errores y llamadas en paralelo

Cómo se tipan async y await en TypeScript: una función async devuelve Promise<T>, await la desenvuelve, los errores se capturan con try/catch, el await de nivel superior necesita un ES module, y la diferencia entre esperar una por una, esperar en paralelo y la trampa de forEach.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Una función async en TypeScript siempre devuelve una promesa: si su cuerpo devuelve un string, su tipo es Promise<string>. Dentro de ella, await desenvuelve una Promise<T> en un T.

El comportamiento en tiempo de ejecución (qué pausa await, el event loop) es JavaScript, y se explica en async/await en JavaScript. Lo que aporta TypeScript son los tipos de entrada y de salida.

Tipos de retorno de las funciones async

El tipo de retorno declarado de una función async debe ser Promise<...>, aunque el cuerpo devuelva el valor directamente. TypeScript lo infiere si lo omites.

Escribir async function count(): number es el error TS1064 (The return type of an async function or method must be the global Promise<T> type. Did you mean to write 'Promise<number>'?). El utility type Awaited<T> da el tipo desenvuelto: Awaited<ReturnType<typeof count>> es number.

Olvidar el await

Un await que falta te deja con una Promise<T> en lugar de un T. TypeScript detecta la mayoría de estos casos porque los tipos dejan de encajar:

El bug del if (ok) es real: un objeto promesa siempre es truthy, así que concedió el acceso. TypeScript lo informa como TS2801. Asignar la promesa a una variable boolean, o leer una propiedad que la promesa no tiene, tampoco compilaría. Una llamada cuyo resultado ignoras (save(user);) no se detecta; de eso se encarga la regla no-floating-promises de typescript-eslint.

Manejo de errores con try/catch

Una promesa rechazada hace que await lance un error, así que el try/catch normal funciona. Con strict, la variable de catch es unknown, y la estrechas antes de leer .message:

Un throw dentro de una función async rechaza su promesa en lugar de lanzar el error en el sitio de la llamada. Hay más patrones, incluidas las clases de error propias y devolver resultados en lugar de lanzar errores, en la página de manejo de errores.

await de nivel superior

await fuera de cualquier función solo funciona en un ES module. Un archivo compilado como CommonJS (el caso de estos ejemplos, y el de los proyectos Node sin "type": "module") lo rechaza:

index.ts(2,14): error TS1309: The current file is a CommonJS module and cannot use 'await' at the top level.

Envuelve el código en una función main async y llámala, como hacen todos los ejemplos de esta página. En un proyecto ES module ("type": "module" en package.json con module en node16 o nodenext, o module: "esnext" para un bundler), el await de nivel superior está permitido.

Secuencial frente a paralelo

Cada await espera a su promesa antes de que empiece la línea siguiente. Para llamadas independientes, inícialas todas primero y espéralas juntas con Promise.all:

En la mitad secuencial, b no puede terminar antes que a porque todavía no ha empezado. En la mitad paralela, d termina primero, y el tiempo total es aproximadamente el de la llamada más larga, no la suma. Promise.all sigue devolviendo los resultados en el orden de entrada, tipados como una tupla.

La trampa de forEach

forEach ignora la promesa que devuelve un callback async, así que nada espera a que termine el trabajo:

for...of con await procesa los elementos de uno en uno; Promise.all con map los ejecuta en paralelo y espera a todos. forEach no hace ninguna de las dos cosas, y TypeScript no avisa, porque un callback tipado para devolver void acepta uno que devuelve una promesa.

Iteración asíncrona con for await

for await...of recorre un iterable asíncrono, como un generador async, esperando cada valor:

async function* pages(total: number): AsyncGenerator<string[]> {
    for (let page = 1; page <= total; page++) {
        await new Promise((r) => setTimeout(r, 10));
        yield [`item ${page}a`, `item ${page}b`];
    }
}

async function main() {
    for await (const batch of pages(3)) {
        console.log(batch.join(", ")); // batch: string[]
    }
}
main();

El tipo de los elementos sale de la anotación AsyncGenerator<T> del generador, o de la inferencia si la omites.

Preguntas frecuentes

¿Cuál es el tipo de retorno de una función async en TypeScript?

Siempre una promesa. Una función async que devuelve un number tiene el tipo de retorno Promise<number>, y una que no devuelve nada tiene Promise<void>. Escribir async function f(): number es el error TS1064, que sugiere Promise<number>.

¿Cómo uso await en el nivel superior en TypeScript?

El await de nivel superior solo funciona en un ES module, con module en es2022, esnext, system, preserve, o node16/node18/node20/nodenext en un archivo que Node trate como ESM, y target en es2017 o superior. En un archivo CommonJS es el error TS1309. La solución portable es una función main async: async function main() { ... } main();.

¿Cómo manejo errores con async/await en TypeScript?

Envuelve el await en try/catch. Con strict, el valor capturado tiene tipo unknown, así que estréchalo primero: if (e instanceof Error) console.log(e.message). Un rechazo al que nunca se hace await ni catch se convierte en un rechazo no manejado.

¿Cómo ejecuto llamadas async en paralelo en TypeScript?

Inicia primero todas las promesas y luego espéralas juntas: const [a, b] = await Promise.all([loadA(), loadB()]). Escribir await loadA(); await loadB(); las ejecuta una detrás de otra. Promise.all conserva el tipo de cada resultado en la tupla resultante.

¿Por qué async no funciona dentro de forEach?

forEach llama al callback e ignora lo que devuelve, así que nunca se espera a las promesas de un callback async: el bucle termina de inmediato y el código posterior se ejecuta antes de que acabe el trabajo. Usa for...of con await para trabajo uno a uno, o await Promise.all(items.map(async (x) => ...)) para trabajo en paralelo.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR