Un artículo del grupo PL de la Universidad Brown, "A Design Space Exploration of Async/Await" (escrito por Gavin Gray), examina sistemáticamente un aspecto que la mayoría de los programadores dan por sentado: async/await. El resultado es inquietante para cualquiera que asuma que la semántica es básicamente la misma en todas partes. Un programa que escribe un registro en segundo plano y luego imprime "C" tras una espera de un segundo produce al menos cuatro resultados diferentes dependiendo del entorno, y en tres variaciones de este mismo programa, ninguno de los siete entornos estudiados coincide en todos los resultados.

Página de documentación de PEP 492 de Python mostrando la exploración del espacio de diseño de la especificación async/await
La Propuesta de Mejora de Python 492 define formalemente las semánticas async/await, separando las coroutines nativas de los generadores para permitir el análisis estático en las implementaciones de tiempo de ejecución de Python.peps.python.org

El artículo discute cómo la PEP 492 de Python separó deliberadamente las coroutines nativas de los generadores, haciendo que el código async sea distinguible estáticamente y amigable con el linter, mientras reconocía las compensaciones entre ergonomía y control semántico en diferentes implementaciones de tiempo de ejecución.

El artículo enmarca la discrepancia en torno a nueve "dimensiones de diseño", agrupadas en tres fases de la vida de una tarea. Las dimensiones de inicio de vida gobiernan cómo se evalúa una llamada async: perezosamente (Python, Rust), con evaluación temprana en el hilo actual (C#, JavaScript) o con garantías de suspensión que varían según el lenguaje. Las dimensiones de fin de vida abarcan el alcance de la tarea (indefinido frente a limitado al alcance de la función que lo crea), la fortaleza de la referencia y cómo se limpia la tarea (esperada, cancelada o terminada directamente). Las dimensiones de cancelación —conciencia, dirección y persistencia— determinan si y cómo una tarea puede responder a que se la cancele.

Dos dimensiones explican la divergencia principal. Los lenguajes de extensión dinámica (Swift, Python+Trio) no permiten que una tarea creada en segundo plano sobreviva al alcance de la función que la generó. Pero al final de ese alcance, Swift cancela la tarea mientras Trio la espera, por lo que Swift imprime "AC" y Trio imprime "ABC" con el mismo texto original. El artículo formaliza estas elecciones en una semántica operativa de pequeño paso, rastreando exactamente dónde cada entorno diverge.

Las especificaciones del lenguaje de soporte confirman que estas decisiones son deliberadas, no accidentales. PEP 492 de Python separó las coroutines nativas de los generadores para hacer que el código async sea distinguible estáticamente y amigable con el linter. RFC 2394 de async/await de Rust enmarca la característica como un azúcar sintáctico sobre futuros, donde una función async retorna un tipo Future anónimo que solo se ejecuta cuando se consulta. Ambos documentos de diseño reconocen las compensaciones que la taxonomía nombra —ergonomía contra control, rendimiento contra semántica— y ninguno afirma que haya una única respuesta universalmente correcta.

La consecuencia práctica: el código que funciona correctamente en un entorno puede cambiar silenciosamente su comportamiento al ser portado, y un patrón de "fire-and-forget" que parece seguro en un lenguaje puede filtrar, cancelarse o bloquearse en otro. Los autores argumentan que las dimensiones son configurables por eje en principio, pero que en la práctica cada lenguaje se compromete con un punto en el espacio y el conocimiento no se transfiere limpiamente, una preocupación que la RFC de Rust mismo reconoce al señalar que los ejes completamente programables convierten cada biblioteca en su propio DSL async.