Async/Await y reject: ¿por qué es un dolor para los principiantes?
\n¿Alguna vez ha escrito una función async, esperó una respuesta del servidor y luego se encontró con un UnhandledPromiseRejection? ¿Le suena familiar? Este es uno de los problemas más comunes que enfrentan los desarrolladores al migrar a async/await en TypeScript.
En Stack Overflow, la pregunta "How to reject in async/await syntax?" ha acumulado miles de visitas. Y con razón. A primera vista, parece que basta con devolver Promise.reject() y listo. Pero en proyectos reales es más complejo: tipos, cadenas de llamadas, pérdida de contexto de errores.
En esta guía, analizaremos cómo rechazar promesas correctamente en funciones async de TypeScript, aprenderemos a diferenciar throw de return Promise.reject() y veremos ejemplos prácticos de proyectos reales.
Fundamentos: cómo funciona reject en async/await
\nAntes de profundizar, recordemos los conceptos básicos. Cualquier función async en TypeScript devuelve un Promise. Si dentro de ella ocurre un throw o se devuelve Promise.reject(), la promesa pasa al estado rejected.
// Ejemplo simple de reject en async/await\nasync function fetchUser(id: number): Promise<User> {\n if (id <= 0) {\n throw new Error('ID de usuario inválido');\n }\n // ...lógica para obtener el usuario\n}\n\nAquí, throw se convierte automáticamente en un reject. Pero, ¿qué pasa si queremos devolver una promesa ya rechazada?
throw vs return Promise.reject()
\nEn TypeScript, ambos enfoques funcionan, pero hay matices:
\n\n- \n
- throw — es la forma más idiomática. Detiene la ejecución de la función y pasa el control al
catchmás cercano. \n - return Promise.reject() — devuelve explícitamente una promesa rechazada. Es útil cuando se necesita pasar un error desde otra función. \n
// Diferencia entre throw y return Promise.reject()\nasync function processOrder(orderId: string): Promise<OrderStatus> {\n // Opción 1: throw\n if (!orderId) {\n throw new Error('Se requiere el ID del pedido');\n }\n \n // Opción 2: return Promise.reject\n if (orderId.startsWith('test_')) {\n return Promise.reject(new Error('Los pedidos de prueba no están permitidos'));\n }\n \n return { status: 'processing' };\n}\n\nImportante: TypeScript no siempre infiere correctamente los tipos con return Promise.reject(). Si la función espera Promise<T> y usted devuelve Promise.reject(), puede ocurrir un error de tipificación. En esos casos, es mejor usar throw.
Errores típicos al hacer reject en funciones async
\nIncluso los desarrolladores experimentados en TypeScript cometen errores clásicos. Aquí están los 3 principales:
\n\n1. Olvidar manejar el reject en el código que llama
\nasync function main() {\n // Mal: sin try/catch\n const user = await fetchUser(-1);\n console.log(user); // ¡UnhandledPromiseRejection!\n}\n\n// Bien:\nasync function mainSafe() {\n try {\n const user = await fetchUser(-1);\n console.log(user);\n } catch (error) {\n console.error('Error al obtener el usuario:', error);\n }\n}\n\n2. Tipificación incorrecta de errores
\nEn TypeScript, el bloque catch por defecto captura unknown. Esto es correcto, pero a veces los desarrolladores intentan acceder a error.message sin verificar.
try {\n await riskyOperation();\n} catch (error) {\n // Error: error es unknown\n console.log(error.message); // ❌ La propiedad 'message' no existe en el tipo 'unknown'\n \n // Correcto:\n if (error instanceof Error) {\n console.log(error.message);\n } else {\n console.log