Working with a JS Promise and a Rust Future
Many APIs on the web work with a Promise, such as an async function in JS.
Naturally you’ll probably want to interoperate with them from Rust! To do that
you can use js_sys::Promise together with Rust async functions.
Awaiting a Promise directly
js_sys::Promise implements IntoFuture, so you can .await it directly
in any async function. This is the recommended approach:
#![allow(unused)]
fn main() {
use js_sys::Promise;
use wasm_bindgen::prelude::*;
async fn get_from_js() -> Result<JsValue, JsValue> {
let promise = Promise::resolve(&42.into());
let result = promise.await?;
Ok(result)
}
}
For typed promises the awaited value matches the type parameter directly:
#![allow(unused)]
fn main() {
use js_sys::{Promise, Number};
async fn get_number() -> Result<Number, JsValue> {
let promise: Promise<Number> = fetch_number();
let number = promise.await?; // number: Number
Ok(number)
}
}
Using JsFuture explicitly
If you need a named Future type — for example to store it in a struct or
pass it around — use JsFuture from js_sys::futures or
wasm_bindgen_futures:
#![allow(unused)]
fn main() {
use js_sys::futures::JsFuture;
use wasm_bindgen::JsValue;
async fn get_from_js() -> Result<JsValue, JsValue> {
let promise = js_sys::Promise::resolve(&42.into());
let result = JsFuture::from(promise).await?;
Ok(result)
}
}
JsFuture is also re-exported from wasm_bindgen_futures for backwards
compatibility:
#![allow(unused)]
fn main() {
use wasm_bindgen_futures::JsFuture;
}
A successful promise becomes Ok and a rejected promise becomes Err,
corresponding to JS .then and .catch.
Importing JS async functions
You can import a JS async function directly with an extern "C" block, and
the promise will be converted to a future automatically. The return type can be
JsValue, no return at all, or Result and Option types to primitives or
types supporting JsCast conversions:
#![allow(unused)]
fn main() {
#[wasm_bindgen]
extern "C" {
async fn async_func_1_ret_number() -> JsValue;
async fn async_func_2();
async fn async_func_3_ret_string() -> JsString;
async fn async_func_4_ret_array() -> Uint8Array;
}
async fn get_from_js() -> f64 {
async_func_1_ret_number().await.as_f64().unwrap_or(0.0)
}
}
The async attribute can be combined with catch to manage errors from the
JS promise:
#![allow(unused)]
fn main() {
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(catch)]
async fn async_func_3() -> Result<JsValue, JsValue>;
#[wasm_bindgen(catch)]
async fn async_func_4() -> Result<(), JsValue>;
}
}
Exporting Rust async fn to JS
Use an async function with #[wasm_bindgen] to export a function that
returns a Promise to JavaScript:
#![allow(unused)]
fn main() {
#[wasm_bindgen]
pub async fn foo() {
// ...
}
}
When invoked from JS the foo function here will return a Promise, so you
can use it as:
import { foo } from "my-module";
async function shim() {
const result = await foo();
// ...
}
Return values of async fn
When using an async fn in Rust and exporting it to JS there are some
restrictions on the return type. The return value of an exported Rust function
will eventually become Result<JsValue, JsValue> where Ok turns into a
successfully resolved promise and Err is equivalent to throwing an exception.
The following types are supported as return types from an async fn:
()— turns into a successfulundefinedin JST: Into<JsValue>— turns into a successful JS valueResult<(), E: Into<JsValue>>—Ok(())resolves toundefined;Errrejects withEconverted to a JS valueResult<T: Into<JsValue>, E: Into<JsValue>>— both payloads are converted into aJsValue
Note that many types implement being converted into a JsValue, such as all
imported types via #[wasm_bindgen] (aka those in js-sys or web-sys),
primitives like u32, and all exported #[wasm_bindgen] types. In general,
you should be able to write code without having too many explicit conversions,
and the macro should take care of the rest!
spawn_local and future_to_promise
Use spawn_local to run a Future<Output = ()> on the current thread
without returning a Promise to JavaScript:
#![allow(unused)]
fn main() {
use js_sys::futures::spawn_local;
spawn_local(async {
// drive a future to completion on the JS microtask queue
});
}
Use future_to_promise to convert a Rust Future into a JavaScript
Promise explicitly:
#![allow(unused)]
fn main() {
use js_sys::futures::future_to_promise;
use wasm_bindgen::JsValue;
let promise = future_to_promise(async {
Ok(JsValue::from(42))
});
}
Both functions are also re-exported from wasm_bindgen_futures unchanged.
Combining JS Promises Concurrently
Rust’s async ecosystem and JavaScript’s promise ecosystem interoperate
cleanly: awaiting a Promise<T> yields to the JS
event loop, so in-flight JS-side work (fetches, timers, I/O) continues in
parallel while Rust futures are pending. futures_util’s join_all /
select / try_join_all work correctly over JsFutures and deliver
proper parallelism.
When your inputs are already JS Promise<T> values, the native
Promise.all / Promise.allSettled / Promise.race bindings on
js_sys::Promise are the idiomatic choice — they match JS semantics
exactly and avoid the Rust executor polling each child on every wake.
The canonical recipe collects the promise-producing iterator straight into a
typed Array<Promise<T>> via [Array::from_iter_typed] and hands it to
the combinator — no turbofish on the elements, no intermediate Vec. The
typed-collection helper infers the element type from the iterator item via
[IntoJsGeneric]; the stable .collect::<Array>() form keeps producing an
erased Array<JsValue> for callers that want erasure.
Promise.all — all must succeed
#![allow(unused)]
fn main() {
use js_sys::{Array, Promise};
// responses: Array<Response>
let responses = Promise::all_iterable(
&Array::from_iter_typed(
(0..10).map(|_| worker.fetch_with_str_and_init(&url, &init)),
),
)
.await?;
}
Rejects with the first rejection, matching Promise.all semantics.
Promise.allSettled — wait for all, never reject early
#![allow(unused)]
fn main() {
use js_sys::{Array, Promise};
// results: Array<PromiseState<Response>>
let results = Promise::all_settled_iterable(
&Array::from_iter_typed(
(0..10).map(|_| worker.fetch_with_str_and_init(&url, &init)),
),
)
.await?;
for state in results.iter() {
if state.is_fulfilled() {
let value = state.get_value().unwrap();
} else {
let reason = state.get_reason().unwrap();
}
}
}
Promise.race — first to settle
#![allow(unused)]
fn main() {
use js_sys::{Array, Promise};
// first: Response
let first = Promise::race_iterable(
&Array::from_iter_typed(
(0..10).map(|_| worker.fetch_with_str_and_init(&url, &init)),
),
)
.await?;
}
If you already have a Vec<Promise<T>>, use Array::of to lift it:
Promise::all_iterable(&Array::of(&promises)).
Heterogeneous Promise.all over a tuple
When the promises resolve to different types, collect them into a Rust
tuple and pass to Promise::all_tuple. The result is a typed
ArrayTuple<(T1, T2, ..., Tn)> you can destructure via .into_tuple()
back into a native Rust tuple. Implemented for arity 1..=8.
#![allow(unused)]
fn main() {
use js_sys::Promise;
// fetch_promise: Promise<Response>
// buffer_promise: Promise<ArrayBuffer>
let (response, buffer) = Promise::all_tuple((fetch_promise, buffer_promise))
.await?
.into_tuple();
}
Rejects with the first rejection, matching Promise.all semantics.
For the Promise.allSettled analogue use Promise::all_settled_tuple; it
resolves to an ArrayTuple<(PromiseState<T1>, ..., PromiseState<Tn>)> and
never rejects early:
#![allow(unused)]
fn main() {
use js_sys::Promise;
let results = Promise::all_settled_tuple((fetch_promise, buffer_promise)).await?;
let (response_state, buffer_state) = results.into_tuple();
if response_state.is_fulfilled() {
let response = response_state.get_value().unwrap();
// ...
}
}
There is no race_tuple equivalent — Promise.race returns whichever input
settles first, whose type would be a union T1 | T2 | ... | Tn that Rust
can’t express. For heterogeneous race, collect into an Array<JsValue> and
use Promise::race_iterable explicitly, then narrow with dyn_into at
the inspection site.
Mixing Rust Futures with JS Promises
If some of your inputs are Rust Future<Output = Result<T, JsValue>> values
rather than JS Promise<T>, lift each one explicitly with
future_to_promise_typed at the call site:
#![allow(unused)]
fn main() {
use js_sys::{futures::future_to_promise_typed, Array, Promise};
// For homogeneous batches, mix both shapes into one `Array<Promise<T>>`:
let responses = Promise::all_iterable(
&Array::from_iter_typed([
fetch_promise,
future_to_promise_typed(async { Ok(fetch_via_rust().await?) }),
]),
)
.await?;
// For heterogeneous tuples, lift each future before passing the tuple:
let (response, buffer) = Promise::all_tuple((
fetch_promise,
future_to_promise_typed(async { Ok(buffer_from_rust().await?) }),
))
.await?
.into_tuple();
}
future_to_promise_typed spawns the Future on the current thread and
returns a JS Promise<T> that settles with its result — from that point on
the two shapes are interchangeable.
Using Generic Promise Types
Promises support erasable generic type parameters. With
IntoFuture, the resolved type flows through directly without a cast:
#![allow(unused)]
fn main() {
use js_sys::{Promise, Number, Array};
#[wasm_bindgen]
extern "C" {
fn fetchNumbers() -> Promise<Array<Number>>;
}
async fn process_numbers() -> Result<f64, JsValue> {
// Await the typed promise — no cast needed
let numbers: Array<Number> = fetchNumbers().await?;
let mut sum = 0.0;
for i in 0..numbers.length() {
sum += numbers.get(i).value_of();
}
Ok(sum)
}
}
Using js_sys directly without wasm-bindgen-futures
To make #[wasm_bindgen] emit js_sys::futures directly and drop the
wasm-bindgen-futures dependency, depend on js-sys and build with
--cfg=wasm_bindgen_use_js_sys (e.g. via RUSTFLAGS or .cargo/config.toml).
A cfg is used rather than a Cargo feature so the choice stays scoped to the
crate that opts in — Cargo features union across the dep graph, which would
flip codegen for every #[wasm_bindgen] user in the build.
Compatibility note
wasm-bindgen-futures is now a thin re-export shim. The futures implementation
lives in js-sys and is always available (no feature gate required). All
existing import paths (wasm_bindgen_futures::JsFuture,
wasm_bindgen_futures::spawn_local, etc.) continue to work without changes.
Learn more: