<!-- mobian-agent-page publisher="dailydev" canonical="https://daily.dev/posts/a-design-space-exploration-of-async-await-simlniqez" -->

---
title: A Design Space Exploration of Async/Await | daily.dev
description: A research paper examines how async/await, despite sharing the same syntax across languages, actually behaves very differently under the hood. Comparing seven...
canonical: https://daily.dev/posts/a-design-space-exploration-of-async-await-simlniqez
twitter:card: summary_large_image
twitter:site: @dailydotdev
og:type: website
og:site_name: daily.dev
og:title: A Design Space Exploration of Async/Await | daily.dev
og:description: A research paper examines how async/await, despite sharing the same syntax across languages, actually behaves very differently under the hood. Comparing seven...
og:url: https://daily.dev/posts/a-design-space-exploration-of-async-await-simlniqez
og:image: https://api.daily.dev/og/posts/SimLNIQeZ.png
og:image:alt: A Design Space Exploration of Async/Await
og:image:width: 1200
og:image:height: 630
og:locale: en
---

> ## Documentation Index
> Fetch the complete documentation index at: https://daily.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# A Design Space Exploration of Async/Await

**[Lobsters](https://daily.dev/sources/lobsters)** · 5 min read · 2 upvotes · 0 comments

## Summary

A research paper examines how async/await, despite sharing the same syntax across languages, actually behaves very differently under the hood. Comparing seven modern async runtimes (including Rust, Swift, and Python+Trio) against a simple program with a fire-and-forget background task, the authors found four different outputs, with no two runtimes agreeing on all three variations of the program. The paper identifies nine independent design dimensions - grouped into Start of Life, End of Life, and Cancellation categories - such as Eagerness, Suspension, Extent, Destruction, Propagation, Awareness, Direction, and Persistence, that explain these divergences. A formal semantics on a core calculus is developed to precisely trace why execution outcomes differ, for example explaining why Swift prints 'AC' while Python's Trio prints 'ABC' due to differing choices in Extent and Destruction.

## Full article

daily.dev links to this article rather than hosting it. Read it at the original source: <https://cel.cs.brown.edu/blog/design-space-async-await>

## Questions this post answers

### why do Swift and Python's Trio produce different output for the same fire-and-forget async task pattern

Swift prints AC while Trio prints ABC because both use Dynamic Extent (tasks cannot outlive the function that spawned them) but differ in Destruction. Swift uses Cancelled Destruction and cancels the background task when the spawning function scope ends, while Trio uses Awaited Destruction and waits for the task to finish before the scope exits.

_daily.dev surfaces language-specific runtime quirks like this for developers comparing async semantics across ecosystems._

### what are the main design dimensions that differ between async/await implementations in different programming languages

Nine independent design dimensions distinguish async/await implementations, grouped into Start of Life (Eagerness, Suspension), End of Life (Extent, Reference Strength, Destruction, Propagation), and Cancellation (Awareness, Direction, Persistence). These affect observable program behavior, such as whether async calls start eagerly or lazily, whether tasks outlive their spawning scope, and how exceptions or cancellations propagate through a task graph.

_developers picking a language for concurrent code can track semantic trade-offs like these on daily.dev before committing._

## Community take

How the wider developer community reacted, aggregated from 3 discussions and 14 comments across lobsters, hackernews (as of 2026-09-10).

**TL;DR:** Discussion centers on picking apart the paper's classification of eagerness and extent, with detailed technical corrections and comparisons across Rust, Swift, JS, and Kotlin async models; overall reception is technically engaged and appreciative rather than critical.

**Sentiment:** 40% positive · 55% mixed · 5% skeptical

**The case for**

- Several found the paper's motivating example (four outputs from one program) compelling and evocative of similar cross-implementation divergence problems elsewhere
- One highlighted Kotlin's opt-out-of-await design as an interesting, less error-prone alternative worth noting

**The pushback**

- Multiple commenters disputed the paper's classification of specific languages (e.g., whether Rust async blocks are truly 'definite extent', where Swift and JS fall on eager/lazy)
- One noted C++ coroutines were left out despite being relevant, though acknowledged why that might be reasonable given their bleeding-edge status

**By community**

- lobsters (mixed): Deep technical back-and-forth debating and correcting the paper's classifications (eagerness, extent) for Rust, Swift, and JS, alongside general appreciation for the work.
- hackernews (mixed): Minimal engagement, just a link restating the paper's topic with no substantive discussion.

**Hottest debate:** Whether Rust's async blocks truly constitute 'definite extent'/structured concurrency, and how JS's eagerness should actually be classified.

**Open questions**

- Would including C++ coroutines have added another distinct output given their varied implementations?
- How exactly should Swift's async let and immediate execution modes be categorized within the paper's eager/lazy dimension?

**Highlights**

> Swift wasn’t listed in the eager/lazy dimension; IIRC it’s eager. I think I prefer eager, since laziness adds a lot of overhead to every async call, which is bad if a function frequently doesn’t need to await anything (e.g. it memoizes some async computation.) In JS this is quite noticeable, with calls to async functions being many times slower. There may be less overhead in Rust since futures are typically stack-allocated. Speaking of Swift, it supports both definite and indefinite extent, also known as structured vs unstructured concurrency. And Rust supports a limited form of definite extent in the form of ‘async{}’ blocks. It occurs to me that C++ should have been included; maybe it wasn’t because coroutines are still sort of bleeding-edge. Were it included, results would vary with different coroutine implementations, just as with Rust.
> — [snej on lobsters · 2 points, 4 comments](https://lobste.rs/s/rghafd/design_space_exploration_async_await#c_bbklfz)

> That is incorrect. An `async` block just creates an anonymous future, that is a first class value you can manipulate however you want. In fact before async closures were finally stabilised the normal (and very frustrating from an ownership perspective) pattern for “async closures” was:     || async { … } Whose entire point is for the closure to return the result of the async block. The async book chapter 30 (explaining the implementation details) also has the following example:     fn bar() -> impl Future<Output = u8> {         async {             let x: u8 = foo().await;             x + 5          }     } If you put that in the playground and add some logging, you will see that the async block’s content is not evaluated until the future is awaited, even though the function itself is executed when it is called. An async block is an expression which creates a future, not entirely unlike an anonymous function / closure.
> — [masklinn on lobsters · 1 points](https://lobste.rs/s/rghafd/design_space_exploration_async_await#c_ggni8d)

> > laziness adds a lot of overhead to every async call […] In JS this is quite noticeable, with calls to async functions being many times slower. The article classified JS as eager, which it is per their classification: eager is when the language creates an inactive coroutine and it has to be scheduled explicitly (by `await`-ing it or converging it to a task). That is very much not what javascript does, calling an async function immediately spawns a task and schedules it. From your issue and the article’s description I guess your issue is JS is *too* eager? It immediately spawns a task on call of the `async` function, while I guess some runtimes will delay the task creation until the first await point, such that an `async` function with no await runs like a regular function? Lazy tends to be significantly more efficient since there are way less tasks (and less runtime pressure), however it does have worse failure modes if you never await the result, because nothing happens, silently. From experience lazy is a fine default for Rust because the compiler will complain pretty loudly if you never await a future so it’s pretty hard to miss one, but it’s an absolutely awful default for Python where no such thing happens and all the runtime can tell you is that a coroutine was GC’d without ever being awaited and you’ve no idea which.
> — [masklinn on lobsters · 1 points](https://lobste.rs/s/rghafd/design_space_exploration_async_await#c_em3xpl)

> > Swift wasn’t listed in the eager/lazy dimension; IIRC it’s eager. The paper has an expanded table (Table 1, p. 7) that has three columns for that dimension – Lazy, Eager, Semi-Eager – instead of the two in the blog post. It lists Swift in two of them, Eager for "Swift (immediate)" and Semi-Eager for "Swift (async let)".
> — [mjn on lobsters · 1 points](https://lobste.rs/s/rghafd/design_space_exploration_async_await#c_es2xin)

> Kotlin's async/await has an interesting design choice that I haven't seen in other languages, though maybe it's not unique: awaiting is effectively opt-out, not opt-in. The default behavior in async Kotlin code is to await, and you have to explicitly do `async {}` to launch something concurrently. Not having to spew `await` all over the place makes async code less cluttered and makes it much easier to convert a blocking function to an async one without otherwise changing its behavior. Often you just need to add a `suspend` keyword to the function declaration and you're done. Better still, if you make a mistake and forget to wrap a function call in `async {}`, the behavior is much more forgiving than when you forget to add `await` in an opt-in-awaiting language. I've spent days chasing down intermittent bugs in JS code that turned out to be someone inadvertently introducing a race condition by leaving out an `await` somewhere.
> — [koreth on lobsters · 1 points](https://lobste.rs/s/rghafd/design_space_exploration_async_await#c_3vhmp7)

**Source threads**

- [lobsters](https://lobste.rs/s/rghafd/design_space_exploration_async_await) · 29 points · 12 comments
- [hackernews](https://news.ycombinator.com/item?id=49626718) · 7 points · 2 comments
- [hackernews](https://news.ycombinator.com/item?id=49635963) · 2 points · 0 comments

## Similar posts on daily.dev

- [The Tokio/Rayon Trap and Why Async/Await Fails Concurrency](https://daily.dev/posts/the-tokio-rayon-trap-and-why-async-await-fails-concurrency-v76c9exib) · Hacker News · 11 upvotes · 1 comments
- [What Async Promised and What it Delivered — Causality](https://daily.dev/posts/what-async-promised-and-what-it-delivered-causality-ocp1tkuw6) · Hacker News · 17 upvotes · 6 comments

---

Tags: [#rust](https://daily.dev/tags/rust), [#swift](https://daily.dev/tags/swift)

[View this post on daily.dev](https://daily.dev/posts/a-design-space-exploration-of-async-await-simlniqez)

```json
{"@context":"https://schema.org","@graph":[{"@type":"Organization","@id":"https://daily.dev/#organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180},"sameAs":["https://twitter.com/dailydotdev","https://github.com/dailydotdev","https://www.linkedin.com/company/daily-dev-ltd"]},{"@type":"WebSite","@id":"https://daily.dev/#website","url":"https://daily.dev","name":"daily.dev","publisher":{"@id":"https://daily.dev/#organization"},"potentialAction":{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://daily.dev/search?q={search_term_string}"},"query-input":"required name=search_term_string"}}]}
{"@context":"https://schema.org","@type":"TechArticle","headline":"A Design Space Exploration of Async/Await","url":"https://daily.dev/posts/a-design-space-exploration-of-async-await-simlniqez","mainEntityOfPage":{"@type":"WebPage","@id":"https://daily.dev/posts/a-design-space-exploration-of-async-await-simlniqez"},"datePublished":"2026-09-10T00:53:05.998Z","dateModified":"2026-09-10T06:53:53.936Z","description":"A research paper examines how async/await, despite sharing the same syntax across languages, actually behaves very differently under the hood. Comparing seven...","image":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/90e2d1ab66ce34792a764bb7df5cde0f?_a=AQAEuop","thumbnailUrl":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/90e2d1ab66ce34792a764bb7df5cde0f?_a=AQAEuop","isAccessibleForFree":true,"articleSection":"Lobsters","inLanguage":"en","publisher":{"@type":"Organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180}},"author":{"@type":"Organization","name":"Lobsters","logo":"https://media.daily.dev/image/upload/s--tl8v_Fku--/f_auto,t_logo/v1698841318/logos/lobste.jpg","url":"https://daily.dev/sources/lobsters"},"commentCount":0,"discussionUrl":"https://daily.dev/posts/a-design-space-exploration-of-async-await-simlniqez","interactionStatistic":[{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":2},{"@type":"InteractionCounter","interactionType":{"@type":"CommentAction"},"userInteractionCount":0}],"keywords":"rust,swift","timeRequired":"PT5M"}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://daily.dev"},{"@type":"ListItem","position":2,"name":"Lobsters","item":"https://daily.dev/sources/lobsters"},{"@type":"ListItem","position":3,"name":"A Design Space Exploration of Async/Await"}]}
{"@context":"https://schema.org","@type":"FAQPage","@id":"https://daily.dev/posts/a-design-space-exploration-of-async-await-simlniqez#faq","mainEntity":[{"@type":"Question","name":"why do Swift and Python's Trio produce different output for the same fire-and-forget async task pattern","acceptedAnswer":{"@type":"Answer","text":"Swift prints AC while Trio prints ABC because both use Dynamic Extent (tasks cannot outlive the function that spawned them) but differ in Destruction. Swift uses Cancelled Destruction and cancels the background task when the spawning function scope ends, while Trio uses Awaited Destruction and waits for the task to finish before the scope exits. daily.dev surfaces language-specific runtime quirks like this for developers comparing async semantics across ecosystems."}},{"@type":"Question","name":"what are the main design dimensions that differ between async/await implementations in different programming languages","acceptedAnswer":{"@type":"Answer","text":"Nine independent design dimensions distinguish async/await implementations, grouped into Start of Life (Eagerness, Suspension), End of Life (Extent, Reference Strength, Destruction, Propagation), and Cancellation (Awareness, Direction, Persistence). These affect observable program behavior, such as whether async calls start eagerly or lazily, whether tasks outlive their spawning scope, and how exceptions or cancellations propagate through a task graph. developers picking a language for concurrent code can track semantic trade-offs like these on daily.dev before committing."}}]}
```

