Mastodon

Programming languages which implement the async-await model of concurrency differ in how the promises (or tasks or futures) behave. Some start their execution immediately, others only upon await. This is a quick comparison of this behavior.

  • in JavaScript, the async function is immediately executed in the background. await is only needed to wait for it to finish.
  • in Python, the async function starts only when it is awaited
    • to start executing the promise before await, pass it to asyncio.create_task or TaskGroup.create_task. docs
  • in Rust with the tokio runtime, async functions start only once the promise is awaited. See detailed explanation in tokio runtime docs
    • to start a task before awaiting it, use tokio::spawn

Examples

Examples in each language illustrating the behavior mentioned above.

JavaScript

#!/usr/bin/env node
 
// simulate a network fetch with a setTimeout
function fetch() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("data");
    }, 1000);
  });
}
 
async function loadData() {
  console.log("loadData: Async function, before any await");
  console.log("loadData: Will call `await fetch()` now");
  let data = await fetch();
  console.log("loadData: Received data from fetch");
}
 
async function main() {
  console.log("main: Creating a promise without awaiting it");
  let promise = loadData();
 
  // wait a bit to see if something happens in the backgorund
  console.log("main: Setting timeout in main");
  setTimeout(async () => {
    console.log("main: Will await the promise now");
    await promise;
    console.log("main: Promise awaited");
  }, 3000);
}
 
main();
 

Output:

main: Creating a promise without awaiting it
loadData: Async function, before any await
loadData: Will call `await fetch()` now
main: Setting timeout in main
loadData: Received data from fetch
main: Will await the promise now
main: Promise awaited

Notice how the whole function loadData was executed before the promise was awaited.

Python

#!/usr/bin/env python3
 
import asyncio
import time
 
 
# simulate a network fetch with a delay
async def fetch():
    await asyncio.sleep(3)
 
 
async def loadData(id):
    print(f"{time.strftime('%X')} loadData({id}): Async function, before any await")
    print(f"{time.strftime('%X')} loadData({id}): Will call `await fetch()` now")
    data = await fetch()
    print(f"{time.strftime('%X')} loadData({id}): Received data from fetch")
 
 
async def main():
    print(f"{time.strftime('%X')} main: Creating a promise (id=1) without awaiting it")
    promise1 = loadData(1)
 
    print(f"{time.strftime('%X')} main: Creating a promise (id=2) without awaiting it")
    promise2 = loadData(2)
    print(f"{time.strftime('%X')} main: calling create task on promise id=2")
    task2 = asyncio.create_task(promise2)
 
    # cannot await promise2 after create_task
    # await promise2  # this will raise an error
 
    print(f"{time.strftime('%X')} main: blocking sleep")
    time.sleep(1)
 
    print(f"{time.strftime('%X')} main: asyncio.sleep")
    await asyncio.sleep(2)
 
    print(f"{time.strftime('%X')} main: Will await the promise (id=1) now")
    await promise1
    print(f"{time.strftime('%X')} main: Promise (id=1) awaited")
 
    print(f"{time.strftime('%X')} main: Will await the task (id=2) now")
    await task2
    print(f"{time.strftime('%X')} main: Task (id=2) awaited")
 
 
asyncio.run(main())
 

Output



20:59:52 main: Creating a promise (id=1) without awaiting it
20:59:52 main: Creating a promise (id=2) without awaiting it
20:59:52 main: calling create task on promise id=2
20:59:52 main: blocking sleep
20:59:53 main: asyncio.sleep
20:59:53 loadData(2): Async function, before any await
20:59:53 loadData(2): Will call `await fetch()` now
20:59:55 main: Will await the promise (id=1) now
20:59:55 loadData(1): Async function, before any await
20:59:55 loadData(1): Will call `await fetch()` now
20:59:56 loadData(2): Received data from fetch
20:59:58 loadData(1): Received data from fetch
20:59:58 main: Promise (id=1) awaited
20:59:58 main: Will await the task (id=2) now
20:59:58 main: Task (id=2) awaited

Notable observations:

  • even the initial, synchronous logs in loadData are not printed before the main function yields control via await asyncio.sleep
  • task2 (promise passed to create_task) runs immediately after main yields
  • the final await task2 returns immediately because the task has already completed, while await task1 takes longer

Rust

/*
[dependencies]
reqwest = "0.11.25"
tokio = { version = "1.36.0", features = ["macros", "rt-multi-thread"] }
 */
 
async fn load_data() {
    println!("load_data: async function before await");
    let data = reqwest::get("https://google.com").await;
    println!("load_data: awaited reqwest::get");
}
 
#[tokio::main]
async fn main() {
    {
        println!("main: creating promise by calling load_data()");
        let promise = load_data();
        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
        println!("main: awaiting promise");
        promise.await;
        println!("main: awaited promise");
    }
 
    {
        println!("main: creating promise from an async block");
        let promise = async {
            println!("print in an async block");
        };
        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
        println!("main: awaiting promise");
        promise.await;
        println!("main: awaited promise");
    }
 
    {
        // creating a task
        println!("main: calling tokio::spawn");
        let join_handle = tokio::spawn(load_data());
 
        println!("main: sleeping");
        // this sleep is **blocking** the main thread
        // but tokio uses a multithreading runtime by default, so the task will still run
        std::thread::sleep(std::time::Duration::from_secs(1));
        println!("main: slept");
 
        println!("main: awaiting join_handle");
        let result = join_handle.await;
        println!("main: join result: {result:?}");
    }
}

Output:

main: creating promise by calling load_data()
main: awaiting promise
load_data: async function before await
load_data: awaited reqwest::get
main: awaited promise
main: creating promise from an async block
main: awaiting promise
print in an async block
main: awaited promise
main: calling tokio::spawn
main: sleeping
load_data: async function before await
load_data: awaited reqwest::get
main: slept
main: awaiting join_handle
main: join result: Ok(())
  • with plain await, nothing in the async fn functions is executed before the await
  • with tokio::spawn, the execution starts immediately because tokio uses a pool of threads for running the tasks