7 puntos por GN⁺ 2025-05-18 | 4 comentarios | Compartir por WhatsApp
  • 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

 
cichol 2025-05-18

await using data = await fn()
el milagro de que await aparezca tanto a la izquierda como a la derecha

 
GN⁺ 2025-05-18
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.dispose y Symbol.asyncDispose, así como DisposableStack y AsyncDisposableStack. 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 JVM

    • No 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) o Aff (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 paralelismo

    • No 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 Heisenbugs en una app multihilo

    • Me 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 estilo Continuation-passing

  • Cuando vi el ejemplo de usar using con una función defer, me pareció muy fresco. Tal vez para mucha otra gente ya sea intuitivo, pero creo que vale la pena mencionarlo

    • Si aprovechas DisposableStack y AsyncDisposableStack incluidos en la propuesta de using, tienes soporte integrado para registrar callbacks. Como using tiene alcance de bloque, eso se necesita para cruzar alcances o para registros condicionales. Pero como una variable using debe inicializarse de inmediato, similar a const, 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, con defer, los recursos usados. Si hace falta, así también puedes ajustar fácilmente el momento de liberación al nivel de la función

    • Se 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 mezclar using y try/catch, o simplemente usar try/catch en 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 adoptar

    • En APIs que no soportan este tipo de función, puedes aplicar using mediante DisposableStack. Cuando manejas varios recursos juntos, sigue siendo mucho más simple que try/catch. Mientras el runtime lo soporte, puedes usarlo de inmediato sin esperar a que actualicen los recursos existentes

    • En 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 bastante

    • TC39 también necesita enfocarse en funciones fundamentales del lenguaje, como los trait/protocol de 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 la orphan rule, pero podría evolucionar hacia una estructura mucho más flexible

    • En el mundo de JavaScript normalmente esto se resuelve con polyfills

  • Me recuerda a C#. A través de IDisposable e IAsyncDisposable, es muy útil para abstracciones como manejo de locks, colas o scopes temporales

    • El 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 statement de C#. La palabra clave using y el método hook dispose ya dan una pista bastante clara

  • Entiendo 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 sintaxis

    • Se 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 Symbol como 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 disposable

    • Se 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 los well known symbol como [Symbol.dispose] garantizan extensibilidad. También se explica como una idea parecida a los métodos __dunder__ de Python

    • Esta 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 usar await o una estructura adecuada de manejo de excepciones. En una librería bien diseñada no se enciman múltiples capas de handlers, y con DisposableStack se puede escribir de forma más concisa. Hoy en día muchas veces ni siquiera hace falta una función async autoejecutable inmediata

    • Si 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 de Symbol. Si existen dos Symbol (síncrono/asíncrono), se plantea el problema de que la abstracción se fuga

    • Los 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 manager

    • Los destructores en otros lenguajes se ejecutan según el momento del GC, así que no son confiables. En cambio, el método dispose se 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 en Symbol evitan 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, como await 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 WeakRef y FinalizationRegistry, pero ni Mozilla recomienda usarlos por lo impredecible que son

    • La 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étodo

  • El 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 error

    • La especificación dice que dispose se ejecuta sin falta ya sea que el bloque termine normalmente o por excepción, bifurcación o salida. Es decir, igual que con using o con try/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 plataforma

    • Visto 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 finally bajo 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 en finally

 
ahwjdekf 2025-05-18

Hasta ahora habíamos vivido perfectamente sin preocuparnos ni un poco por estas cosas de los recursos. ¿Qué te pasó de repente?