Escribiendo Cadenas Async Más Limpias con Promise.try
Si alguna vez has escrito una cadena de promesas que comienza con una función que podría ser síncrona o podría ser asíncrona, probablemente te has encontrado con un problema incómodo: ¿dónde colocas tu .catch()?
Los errores síncronos lanzados antes de que se devuelva una promesa no serán capturados por .catch() a menos que ya estés dentro de un contexto de promesa. Promise.try() resuelve esto de manera elegante al proporcionarte un único punto de entrada consistente para cualquier cadena de promesas, independientemente de si la función que estás llamando es síncrona, asíncrona o algo intermedio.
Puntos Clave
- Los throws síncronos dentro de funciones que a veces devuelven promesas pueden escapar completamente de
.catch(), generando excepciones no manejadas. Promise.try()ejecuta una función inmediatamente y envuelve tanto los retornos síncronos como los throws síncronos en una promesa adecuada, proporcionándote una ruta unificada de manejo de errores.- A diferencia de
Promise.resolve(fn()), captura errores síncronos. A diferencia dePromise.resolve().then(fn), se ejecuta inmediatamente sin diferir a una microtarea. - Es más adecuado para cadenas basadas en
.then()con puntos de entrada síncronos/asíncronos mixtos, no como reemplazo deasync/await.
El Problema: Errores Síncronos que Escapan de Tu Cadena .catch()
Considera un cargador de datos que lee desde una caché de forma síncrona o consulta una API de forma asíncrona dependiendo de las condiciones:
function loadData(key) {
const cached = getFromCache(key) // puede lanzar síncronamente
if (cached) return cached
return fetch(`/api/data/${key}`).then(res => res.json())
}
loadData('user-1')
.then(data => render(data))
.catch(err => handleError(err)) // ⚠️ No capturará throws síncronos de loadData
Si getFromCache lanza un error síncronamente, ese error nunca es capturado por .catch(). El throw ocurre antes de que exista cualquier promesa, por lo que escapa completamente de la cadena y se convierte en una excepción no manejada.
Hay una segunda sutileza aquí que vale la pena mencionar: cuando loadData devuelve un valor en caché directamente (un no-thenable), llamar a .then() sobre él también fallará porque los valores simples no tienen un método .then(). Esta función es inherentemente frágil: devuelve una promesa en una rama y un valor crudo en otra. Promise.try() aborda ambos problemas al producir siempre una promesa.
Cómo Promise.try() Soluciona Esto
Promise.try(fn) ejecuta la función proporcionada inmediatamente y envuelve el resultado en una promesa. Si la función devuelve un valor simple, se resuelve con ese valor. Si devuelve una promesa, adopta esa promesa. Si lanza un error síncronamente, convierte ese error en un rechazo.
Esto te proporciona un único punto de entrada para manejar tanto errores síncronos como asíncronos a través del mismo .catch():
Promise.try(() => loadData('user-1'))
.then(data => render(data))
.catch(err => handleError(err)) // ✅ Captura tanto throws síncronos como rechazos asíncronos
Sin casos especiales. Sin envolver en try/catch antes de iniciar la cadena. Todo fluye a través de .catch() como se espera.
Discover how at OpenReplay.com.
Cómo Difiere de Promise.resolve().then(fn) y Promise.resolve(fn())
Estos dos patrones se usan comúnmente como soluciones alternativas, pero se comportan de manera diferente en aspectos importantes.
Promise.resolve(fn()) llama a fn() inmediatamente, fuera de un contexto de promesa. Un throw síncrono aquí es una excepción no capturada, no un rechazo.
Promise.resolve().then(fn) difiere la ejecución de fn a una microtarea. Esto significa que fn no se ejecuta inmediatamente, lo que puede causar problemas sutiles de temporización y hace que el comportamiento sea menos predecible cuando necesitas ejecución inmediata.
Promise.try(fn) ejecuta fn inmediatamente y captura cualquier throw síncrono como un rechazo. Es el más predecible de los tres para iniciar cadenas de promesas.
| Patrón | Ejecuta fn inmediatamente | Captura throws síncronos |
|---|---|---|
Promise.resolve(fn()) | ✅ | ❌ |
Promise.resolve().then(fn) | ❌ | ✅ |
Promise.try(fn) | ✅ | ✅ |
Casos de Uso Prácticos en Frontend
Promise.try() encaja naturalmente en patrones asíncronos de JavaScript donde el comportamiento depende de condiciones en tiempo de ejecución:
Funciones de utilidad que pueden devolver datos en caché síncronamente o consultarlos asíncronamente:
Promise.try(() => getUserFromCacheOrAPI(userId))
.then(updateUI)
.catch(showErrorBanner)
Flujos de trabajo asíncronos condicionales donde un paso de validación podría lanzar un error antes de que comience cualquier trabajo asíncrono:
Promise.try(() => {
validateInput(formData) // lanza si es inválido
return submitForm(formData) // devuelve una promesa
})
.then(handleSuccess)
.catch(handleError)
Compatibilidad y Soporte en Navegadores
Promise.try() fue incluido en ECMAScript 2025 (ES2025) y es compatible con Chrome 128+, Firefox 134+, Safari 18.2+ y Node.js 22.7.0+. Puedes verificar el soporte actual en navegadores en Can I Use, que rastrea el estado de implementación en los principales navegadores y entornos de ejecución.
Para entornos más antiguos, puedes crear un polyfill con un wrapper simple:
Promise.try = Promise.try || function(fn) {
return new Promise(resolve => resolve(fn()))
}
Esto funciona porque el executor de new Promise se ejecuta síncronamente, por lo que fn() se llama inmediatamente. Si fn() lanza un error, el constructor Promise lo captura y lo convierte en un rechazo. Si fn() devuelve un thenable, resolve lo adopta.
Conclusión
Promise.try() no es un reemplazo para async/await. Es una herramienta pequeña y enfocada para una situación específica: iniciar una cadena de promesas cuando el punto de entrada podría lanzar síncronamente o devolver una mezcla de valores y promesas.
Si ya estás dentro de una función async, un bloque try/catch maneja ambos casos naturalmente. Pero cuando estás trabajando con cadenas .then(), especialmente alrededor de funciones de utilidad o cargadores de datos con lógica condicional, Promise.try() mantiene tu manejo de errores consistente y tus cadenas limpias.
Preguntas Frecuentes
Sí. Si la función que pasas a Promise.try() es async, devuelve una promesa, y Promise.try() adopta esa promesa. Funciona igual que pasar cualquier función que devuelva promesas. El principal beneficio de Promise.try() es para funciones que podrían no devolver una promesa en absoluto, o que podrían lanzar un error antes de devolver una.
No. Dentro de una función async, un bloque try/catch estándar ya captura tanto throws síncronos como rechazos esperados (awaited). Promise.try() está diseñado para cadenas estilo .then() donde necesitas un punto de entrada seguro. Si ya estás usando async/await, probablemente no necesites Promise.try().
El polyfill común usando new Promise(resolve => resolve(fn())) es funcionalmente equivalente a la implementación nativa. Ejecuta fn inmediatamente, captura throws síncronos a través del constructor Promise y adopta thenables mediante resolve. Es seguro para uso en producción en entornos que carecen de soporte nativo.
Promise.resolve(fn()) llama a fn fuera de un contexto de promesa, por lo que los throws síncronos se convierten en excepciones no capturadas. Promise.resolve().then(fn) captura throws pero difiere la ejecución a una microtarea, lo que significa que fn no se ejecuta inmediatamente. Promise.try(fn) es el único patrón que tanto ejecuta fn inmediatamente como captura errores síncronos como rechazos.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.