- La propuesta de Explicit Resource Management introduce una nueva forma de controlar claramente el ciclo de vida de recursos como manejadores de archivos y conexiones de red
- Esta función puede usarse desde Chromium 134 y V8 v13.8
- Elementos que se agregan al lenguaje
- Las declaraciones
using y await using, junto con los símbolos Symbol.dispose y Symbol.asyncDispose, proporcionan un mecanismo de limpieza automática
DisposableStack y AsyncDisposableStack permiten agrupar y liberar múltiples recursos de forma segura
SuppressedError permite gestionar conjuntamente los errores ocurridos durante la limpieza y los errores existentes
- Este enfoque mejora mucho la seguridad del código y su mantenibilidad, y es efectivo para prevenir fugas de recursos
- Simplifica el patrón tradicional de try...finally y permite un manejo de recursos confiable en entornos complejos con muchos recursos
Resumen de la propuesta de gestión explícita de recursos
- La propuesta de Explicit Resource Management introduce una nueva forma de crear y liberar claramente recursos como manejadores de archivos y conexiones de red
- Sus componentes principales son los siguientes
- Declaraciones
using y await using: liberan automáticamente los recursos al salir del scope
- Símbolos
[Symbol.dispose]() y [Symbol.asyncDispose]() : métodos para implementar la operación de liberación (cleanup)
- Objetos globales DisposableStack y AsyncDisposableStack: agrupan múltiples recursos para administrarlos de manera eficiente
SuppressedError: nuevo tipo de error que incluye tanto los errores ocurridos durante la limpieza de recursos como el error original
- Estas funciones se enfocan en que los desarrolladores puedan gestionar recursos con más detalle y mejorar el rendimiento y la seguridad del código
Declaraciones using y await using
- La declaración
using se usa para recursos síncronos, y await using para recursos asíncronos
- Los recursos declarados invocan automáticamente Symbol.dispose o Symbol.asyncDispose al salir del scope
- Esto ayuda a reducir los problemas de fugas de recursos síncronos/asíncronos y a escribir código de liberación consistente
- Estas palabras clave solo pueden usarse dentro de bloques de código, loops
for y cuerpos de funciones; no pueden usarse en el nivel superior
- Ejemplo
- Por ejemplo, al usar
ReadableStreamDefaultReader, es obligatorio llamar a reader.releaseLock() para poder reutilizar el stream
- Si ocurre un error y se omite esta llamada, el stream puede quedar bloqueado permanentemente
- Forma tradicional
- El desarrollador usa un bloque try...finally para garantizar la liberación del bloqueo del reader
- Es necesario escribir
reader.releaseLock() en el bloque finally
- Forma mejorada: introducción de
using
- Se crea un objeto disposable (readerResource) que incluye la operación de liberación
- Si se usa el patrón
using readerResource = {...}, se libera automáticamente en cuanto se sale del bloque de código
- Si en el futuro las Web API agregan soporte para
[Symbol.dispose] y [Symbol.asyncDispose], podría ser posible la gestión automática sin necesidad de escribir un objeto wrapper por separado
DisposableStack y AsyncDisposableStack
- Se introducen
DisposableStack y AsyncDisposableStack para agrupar múltiples recursos de manera eficiente y segura
- Al agregar recursos a cada stack y liberar el propio stack, todos los recursos internos se liberan en orden inverso
- Esto reduce el riesgo y simplifica el código al manejar conjuntos complejos de recursos con dependencias entre sí
- Métodos principales
use(value): agrega un recurso disposable en la parte superior del stack
adopt(value, onDispose): agrega un recurso no disposable junto con un callback de liberación
defer(onDispose): agrega solo una operación de liberación sin recurso asociado
move(): mueve todos los recursos del stack actual a un nuevo stack, permitiendo transferir la propiedad
dispose(), asyncDispose(): liberan todos los recursos dentro del stack
Estado de soporte y momento de uso
- La gestión explícita de recursos puede usarse en Chromium 134 y V8 v13.8 o superiores
- Se espera que en el futuro aumente su compatibilidad con diversas Web API
4 comentarios
await using data = await fn()el milagro de que
awaitaparezca tanto a la izquierda como a la derechahttps://typescriptlang.org/docs/handbook/…
Comentarios de Hacker News
Esta propuesta transmite una sensación similar al problema de los "colores de función". La distinción entre funciones síncronas y asíncronas sigue invadiendo toda la funcionalidad. Por ejemplo, se puede ver en los casos de
Symbol.disposeySymbol.asyncDispose, así comoDisposableStackyAsyncDisposableStack. Me gusta que Java haya ido por la ruta de los hilos virtuales (virtual threads). Me parece una decisión que reduce la carga para desarrolladores de aplicaciones, autores de librerías y depuradores al añadir complejidad a la JVMNo estoy de acuerdo en que ocultar la asincronía ayude, porque eso hace más difícil entender el flujo del código. Quiero saber si un recurso se libera de forma asíncrona y si eso puede verse afectado por factores externos, como problemas de red
De verdad me fastidia esta idea, tan común hoy en la mayoría de los lenguajes, de que "todo el código debe escribirse de forma asíncrona". Veo a Purescript como el único caso donde puedes escribir el código con
Eff(efectos síncronos) oAff(efectos asíncronos) y decidir al momento de invocarlo. La concurrencia estructurada (Structured concurrency) está padre, pero en la práctica se parece más a un trabajo para tener varios manejadores de solicitudes de nivel superior en un servidor que a una tarea sintáctica para obtener concurrencia estructurada. No es más que un medio para facilitar el paralelismoNo sé cómo se implementó en la JVM, pero en general el multithreading es una tecnología realmente poco intuitiva de manejar. Hay montones de libros sobre condiciones de carrera, deadlocks, livelocks, starvation y problemas de visibilidad de memoria. Comparado con eso, la programación asíncrona de un solo hilo impone mucha menos carga. Aceptar el problema de los colores de función es menos doloroso que depurar
Heisenbugsen una app multihiloMe da muchísimo gusto que Java haya tomado esa decisión
La explicación es que la ejecución normal y las funciones asíncronas forman categorías cartesianas cerradas (
closed Cartesian categories) entre sí. La categoría de ejecución normal puede incrustarse directamente en la categoría asíncrona. Toda función pertenece a una categoría —es decir, tiene un color de función— y algunos lenguajes lo muestran de manera más explícita. Es una decisión de diseño del lenguaje, y la teoría de categorías puede aplicarse con fuerza más allá del threading. Java y los enfoques basados en hilos terminan enfrentando problemas de sincronización, y eso es especialmente difícil. JavaScript restringe una categoría monádica, en particular el estiloContinuation-passingCuando vi el ejemplo de usar
usingcon una funcióndefer, me pareció muy fresco. Tal vez para mucha otra gente ya sea intuitivo, pero creo que vale la pena mencionarloSi aprovechas
DisposableStackyAsyncDisposableStackincluidos en la propuesta deusing, tienes soporte integrado para registrar callbacks. Comousingtiene alcance de bloque, eso se necesita para cruzar alcances o para registros condicionales. Pero como una variableusingdebe inicializarse de inmediato, similar aconst, no permite inicialización condicional. En esos casos, hace falta el patrón de crear un Stack al inicio de la función y subir al stack, condefer, los recursos usados. Si hace falta, así también puedes ajustar fácilmente el momento de liberación al nivel de la funciónSe siente parecido a golang
Me parece una idea realmente buena, pero incluso si en el futuro se pudiera integrar
[Symbol.dispose]y[Symbol.asyncDispose]en cosas como los streams de la Web API, en el futuro cercano solo algunas APIs y librerías soportarán la funcionalidad y el resto —la mayoría— no. Al final quedas con el dilema de mezclarusingytry/catch, o simplemente usartry/catchen todo el código para que sea más fácil de entender. Existe el riesgo de que, por eso, esta función gane la reputación de ser "impráctica". Da pena porque es un buen diseño que resuelve un problema real, pero aun así podría ser difícil de adoptarEn APIs que no soportan este tipo de función, puedes aplicar
usingmedianteDisposableStack. Cuando manejas varios recursos juntos, sigue siendo mucho más simple quetry/catch. Mientras el runtime lo soporte, puedes usarlo de inmediato sin esperar a que actualicen los recursos existentesEn JavaScript esto se ha repetido durante 15 años. Las nuevas funciones del lenguaje primero llegan a compiladores como Babel, luego entran en la especificación y finalmente tardan 3 o 4 años en llegar a APIs estables y navegadores. De todos modos, los desarrolladores ya están acostumbrados a envolver Web APIs con pequeños wrappers, y muchas veces eso es mejor que un polyfill. Nunca he pensado "va a ser difícil usarlo" solo porque apareció una nueva función útil del lenguaje
De hecho, muchas funciones ya están implementadas como polyfill, y gran parte del ecosistema de NodeJS usa este patrón; los usuarios incluso ajustan solo la sintaxis mediante transpiladores. Mientras preparaba una charla sobre esto el año pasado, descubrí que NodeJS y varias librerías importantes ya tienen bastantes APIs con soporte para
Symbol.dispose. En frontend quizá se use menos porque ya existen sistemas de manejo de ciclo de vida, pero en ciertas situaciones sigue siendo útil. Creo que en librerías de pruebas o en backend sí se va a extender bastanteTC39 también necesita enfocarse en funciones fundamentales del lenguaje, como los
trait/protocolde Rust. En Rust es relativamente fácil definir e implementar nuevos traits, y en JS, al ser un lenguaje dinámico con símbolos únicos, debería poder introducirse de forma todavía más simple. Tiene desventajas como laorphan rule, pero podría evolucionar hacia una estructura mucho más flexibleEn el mundo de JavaScript normalmente esto se resuelve con polyfills
Me recuerda a C#. A través de
IDisposableeIAsyncDisposable, es muy útil para abstracciones como manejo de locks, colas o scopes temporalesEl autor de la propuesta viene de Microsoft, así que la sintaxis se definió de forma parecida a C#. También se ve ese mismo contexto de manera consistente en los issues relacionados de GitHub
Básicamente es un diseño tomado de C#. La propuesta original también hace referencia al context manager de Python, al try-with-resources de Java y al
using statementde C#. La palabra claveusingy el método hookdisposeya dan una pista bastante claraEntiendo que para JavaScript es importante mantener la compatibilidad hacia atrás, pero la sintaxis
[Symbol.dispose]()se siente rara. Me confunde como si hubiera handles de métodos dentro de un arreglo. Me da curiosidad saber qué es exactamente esta sintaxisSe explica que en los literales de objeto las claves dinámicas envueltas entre corchetes ya se usan desde hace casi 10 años, desde ES6. Además, como a los símbolos no se les puede referenciar por cadena, se combina una clave dinámica con la sintaxis abreviada de métodos. En el fondo, no es una sintaxis nueva
Con buen material de respaldo, se dice que esto viene de la forma de asignar claves de símbolo a objetos existentes. Es una evolución natural
Otras personas ya explicaron qué es, pero parecía faltar la explicación de por qué. Si usas un
Symbolcomo nombre de método, garantizas que es una API nueva sin colisiones con métodos existentes. También evita que por accidente una clase sea tratada incorrectamente como disposableSe menciona el concepto de acceso dinámico a propiedades (
dynamic property access). Las propiedades de objeto pueden accederse con punto (.) o con corchetes ([]), y soportan tanto cadenas como símbolos. Los símbolos se comparan como objetos únicos, y loswell known symbolcomo[Symbol.dispose]garantizan extensibilidad. También se explica como una idea parecida a los métodos__dunder__de PythonEsta sintaxis ya lleva varios años en uso. El iterador de JavaScript funciona de la misma manera, y se introdujo hace casi 10 años
Presentan por qué han estado intentando introducir concurrencia estructurada en JS, sobre todo cuando el manejo de recursos y el alcance léxico son características clave. También comparten una librería relacionada de concurrencia estructurada
Bun ya soporta esta función desde la versión 1.0.23 en adelante. Se puede probar de forma experimental
No entiendo en absoluto cómo se puede comprender y controlar el flujo de ejecución de un programa con un estilo de código tan complicado
Ese es justo el punto. El 90% del desarrollo web son upgrades inútiles o que nadie pidió, y luego la realidad es arreglar los problemas que eso crea con el 10% del tiempo. De vez en cuando, con baja probabilidad, alguien tiene que ver código escrito hace mucho tiempo, y ahí recomiendan dejar el bug como ejercicio de entrada para un junior. Incluso sistemas legacy de hace 20 años siguen en uso
El código presentado como ejemplo tiene muchos errores graves de sintaxis y está lejos de parecer JavaScript real. Y los desarrolladores de JS no suelen mezclar las cosas así (
while, promise chain,finally, etc.); lo normal es usarawaito una estructura adecuada de manejo de excepciones. En una librería bien diseñada no se enciman múltiples capas de handlers, y conDisposableStackse puede escribir de forma más concisa. Hoy en día muchas veces ni siquiera hace falta una función async autoejecutable inmediataSi trabajas profesionalmente con ese lenguaje y te acostumbras al significado y comportamiento de sus palabras clave, entiendes el código de manera natural. A los programadores de Haskell les pasa algo parecido
Al incrustar código en HN, cada línea necesita al menos 2 espacios de sangría. (Aunque sí coincido en que el código es difícil de entender)
Un consejo breve: la sangría ayuda
Me pregunto por qué no eligieron un destructor de clase anónima (
anonymous) o alguna estructura distinta deSymbol. Si existen dosSymbol(síncrono/asíncrono), se plantea el problema de que la abstracción se fugaLos destructores requieren un comportamiento predecible —que el cleanup ocurra de forma clara—, y los GC avanzados (
garbage collector) no encajan bien con ese patrón. Los lenguajes modernos soportan cleanup basado en scope, y lo implementan de muchas formas: HoF (funciones de orden superior), hooks especiales, registro de callbacks, etc. Python al principio se basaba en destructores (refcount GC), pero por sus limitaciones terminó introduciendo el context managerLos destructores en otros lenguajes se ejecutan según el momento del GC, así que no son confiables. En cambio, el método
disposese llama claramente cuando termina el scope de la variable, así que es predecible para cosas como cerrar archivos o liberar locks. Los métodos basados enSymbolevitan colisiones con funciones existentes, y normalmente basta con que el desarrollador de la librería se encargue. La separación entre síncrono y asíncrono debe ser clara, y eso puede requerir una sintaxis algo extraña, comoawait using a = await b()En lenguajes con GC, los destructores casi siempre son no deterministas porque es difícil llamarlos de forma síncrona. En JS existen
WeakRefyFinalizationRegistry, pero ni Mozilla recomienda usarlos por lo impredecible que sonLa fortaleza de este enfoque es que también puede usarse con objetivos que no sean instancias de clase
En JavaScript no existe el concepto de propiedad anónima (
anonymous property), así que la pregunta misma se siente ambigua. Se afirma que no hay alternativa a este métodoEl primer ejemplo de la propuesta muestra código que libera un lock de forma segura con
try/finally. Me pregunto si este patrón solo es importante en situaciones de larga ejecución, o si en navegador o CLI el lock también se libera cuando el proceso termina por errorLa especificación dice que
disposese ejecuta sin falta ya sea que el bloque termine normalmente o por excepción, bifurcación o salida. Es decir, igual que conusingo contry/finally. La terminación forzada —matar el proceso— queda fuera del alcance de la especificación, así que ECMAScript no interviene. El stream del ejemplo es un objeto interno de JS, así que si el intérprete desaparece, el concepto mismo de lock deja de tener sentido. Si fueran recursos del SO (memoria, archivos, etc.), normalmente el sistema operativo los limpia en conjunto, aunque el comportamiento varía según la plataformaVisto de otra forma, una página web en el navegador es una aplicación de ejecución muy prolongada. Incluso dura más que muchos procesos de servidor. Si ocurre un error, la página no muere, y el manejo de errores, incluidas las excepciones, se procesa en
finallybajo reglas claras. En NodeJS, por defecto el proceso termina ante un error, pero en entornos de servidor son comunes otros manejos. Es decir, la función de liberación sí se llama obligatoriamente enfinallyHasta ahora habíamos vivido perfectamente sin preocuparnos ni un poco por estas cosas de los recursos. ¿Qué te pasó de repente?