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.
awaitis 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 toasyncio.create_taskorTaskGroup.create_task. docs
- to start executing the promise before
- 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
- to start a task before awaiting it, use
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
loadDataare not printed before themainfunction yields control viaawait asyncio.sleep - task2 (promise passed to
create_task) runs immediately aftermainyields - the final
await task2returns immediately because the task has already completed, whileawait task1takes 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 theasync fnfunctions is executed before the await - with
tokio::spawn, the execution starts immediately because tokio uses a pool of threads for running the tasks