The sync-WebAssembly mismatch
System languages—C, C++, Rust, and most others—expose I/O through synchronous APIs. A file read, network request, or console write is a blocking call: the program makes the call and does not proceed until the result is returned. That is a workable model when the underlying hardware responds quickly enough, but the same is not true on the web.
The web's execution environment is single-threaded when it comes to user code. Any long-running operation that shares the thread with layout, rendering, and event handling would freeze the entire page if it blocked until completion. The platform therefore requires I/O to be asynchronous: you schedule the operation, register a callback, and return control to the browser's event loop. When the operation finishes, the callback is queued and executed on a subsequent loop iteration.
WebAssembly adds a further constraint. The language has no built-in mechanism to suspend and resume execution across an asynchronous boundary. Code calling an asynchronous web API from Wasm cannot simply await a Promise; the Wasm module has to keep its call stack intact while the browser processes other tasks. That is exactly the gap Asyncify fills.
Bridging with Asyncify
Asyncify is a binary transformation that rewrites a Wasm module so that it can be suspended at a program point and later resumed with its state intact. It is typically used by Emscripten and other toolchains to let synchronous-language code call asynchronous JavaScript APIs.
Consider a C function that opens a file, reads a name, and prints a greeting. The toolchain maps fopen and fread to asynchronous web storage APIs—for example, the File System Access API or IndexedDB—which return Promises. Without Asyncify, the compiled Wasm would have to return to JavaScript immediately after initiating the read, losing the state of the C function that made the call. Asyncify instead instruments the module so that when an asynchronous operation is pending, the module saves its entire call stack and returns to the host. Once the Promise resolves, the runtime restores the stack and resumes execution exactly where it left off.
This transformation applies recursively. A call chain that starts in a synchronous function, goes through an imported asynchronous function, and continues after the result is available—all of that is handled by the instrumented code. The developer writing in C or Rust continues to use ordinary blocking I/O calls; Asyncify makes the translation to asynchronous web APIs transparent.
How suspension works under the hood
Asyncify does not extend the WebAssembly specification. It rewrites the module itself, adding a program counter and a stack frame for each suspend point. When a call to an asynchronous import is made, the instrumented code checks whether the result is already available. If not, it saves the current execution point and unwinds the stack to the module boundary. Control returns to JavaScript, where the Promise from the underlying API is awaited normally.
When the Promise settles, the runtime calls back into the module with the pending result. Asyncify locates the saved frame, restores the local variables and the program counter, and continues execution as if no interruption happened. The synchronous caller never observes the gap.
The cost is a modest amount of added code size and some bookkeeping at runtime. The savings are substantial: without Asyncify, developers would have to split every function that touches I/O into callback-based pieces, which is impractical for most existing codebases.
Practical use in Emscripten
Emscripten ships with Asyncify support and turns it on when a project uses APIs like emscripten_sleep() or emscripten_fetch() that need asynchronous behavior. The toolchain guides which functions must be instruments; marking a function with the EM_ASYNC_JS macro or using the asyncify attribute on imports tells the transformation where asynchronous boundaries may occur.
A common example is a sleep function. A naive implementation that blocks the thread until a timer expires is incorrect on the web; it would freeze the UI. The idiomatic JavaScript equivalent uses setTimeout() with a callback. With Asyncify, the synchronous C or Rust code can call an import that returns a Promise resolved by setTimeout(). The Wasm module suspends, the browser continues to render and handle events, and after the timer fires, the module resumes.
Avoiding dead code and nested instruments
Asyncify requires careful interop with imported functions that might themselves trigger asynchronous work transitively. If a synchronous import internally calls another asynchronous function that is not instrumented, the transformation may fail to suspend correctly. Toolchains mitigate this by requiring all imports on the path to be annotated or by instrumenting the entire module conservatively.
Performance is another consideration. Suspension and resumption add overhead at every potentially-asynchronous call, so marking only the necessary functions is better than instrumenting everything. In practice, the set of calls that can suspend is usually small: the imports from the host environment, plus any synchronous code that calls them.
Takeaways
- Web I/O APIs are asynchronous by design because the event loop must stay responsive.
- System-language I/O is synchronous, which creates a mismatch when compiling to WebAssembly.
- Asyncify transforms a Wasm module to allow suspension at asynchronous import calls and resumes later with state intact.
- Emscripten integrates Asyncify transparently, letting C and Rust code use ordinary blocking calls over asynchronous web APIs.
Making synchronous Wasm talk to async web APIs
Asyncify is a compile-time feature in Emscripten that lets you pause an entire WebAssembly program and resume it asynchronously later. This bridges the fundamental gap between synchronous Wasm code and the asynchronous APIs of the web platform.
Using Asyncify from C and C++
To implement an asynchronous sleep in C with Emscripten, you define a JavaScript snippet as if it were a C function using EM_JS. Inside that snippet, you call Asyncify.handleSleep(), passing it a wakeUp() handler that fires when the async operation completes. That handler can be passed to any callback-based API, such as setTimeout(), and then async_sleep() behaves like a regular synchronous call.
#include <stdio.h>
#include <emscripten.h>
EM_JS(void, async_sleep, (int seconds), {
Asyncify.handleSleep(wakeUp => {
setTimeout(wakeUp, seconds * 1000);
});
});
…
puts("A");
async_sleep(1);
puts("B");
Compiling this code requires telling Emscripten to enable Asyncify with -s ASYNCIFY and specifying the functions that may be asynchronous via -s ASYNCIFY_IMPORTS=[func1, func2]. This lets the compiler know it needs to inject state-saving and restoring code around calls to those functions.
emcc -O2 \
-s ASYNCIFY \
-s ASYNCIFY_IMPORTS=[async_sleep] \
...
Executing this in the browser yields the expected sequential log—B appears after a short delay following A.
A
B
Returning values and handling complex types
To return a value from an Asyncify function, return the result of handleSleep() and pass the result to the wakeUp() callback. This works for number-based results, but you can extend it to handle promises directly: instead of Asyncify.handleSleep(), call Asyncify.handleAsync(). This accepts an async JavaScript function, letting you use await and return naturally inside it.
EM_JS(int, get_answer, (), {
return Asyncify.handleAsync(async () => {
let response = await fetch("answer.txt");
let text = await response.text();
return Number(text);
});
});
int answer = get_answer();
For more complex values like strings, Emscripten's Embind feature handles conversions between JavaScript and C++ values and supports Asyncify. You can call await() on external promises and it behaves like await in JavaScript. This approach doesn't require the ASYNCIFY_IMPORTS flag, as it's included by default.
val fetch = val::global("fetch");
val response = fetch(std::string("answer.txt")).await();
val text = response.call<val>("text").await();
auto answer = text.as<std::string>();
Using Asyncify from Rust and other toolchains
The Asyncify transform is toolchain-agnostic. It operates on arbitrary WebAssembly files regardless of the compiler. The transform ships as part of wasm-opt from the Binaryen toolchain, invoked with --asyncify and a comma-separated list of asynchronous functions via --pass-arg=…. These are the functions where the program state should be suspended and later resumed.
After defining an async import in Rust via an extern block, you compile to WebAssembly and then apply the transform:
extern {
fn get_answer() -> i32;
}
println!("Getting answer...");
let answer = get_answer();
println!("Answer is {}", answer);
cargo build --target wasm32-unknown-unknown
wasm-opt -O2 --asyncify \
[email protected]_answer \
[...]
You then need supporting runtime glue code to perform the actual suspension and resumption. A library for this is available on GitHub at https://github.com/GoogleChromeLabs/asyncify and on npm as asyncify-wasm. It mimics the standard WebAssembly instantiation API but accepts asynchronous imports. When the Wasm module calls such a function, the library detects a returned Promise, saves the application state, subscribes to the promise, and restores execution once resolved.
Because any function in the module might make an asynchronous call, all exports become potentially asynchronous and get wrapped. You must await the result of something like instance.exports.main() to know when execution truly finishes.
const { instance } = await Asyncify.instantiateStreaming(fetch('app.wasm'), {
env: {
async get_answer() {
let response = await fetch("answer.txt");
let text = await response.text();
return Number(text);
}
}
});
…
await instance.exports.main();
Under the hood of Asyncify
When Asyncify detects a call to one of the ASYNCIFY_IMPORTS functions, it starts the async operation, saves the full application state—including the call stack and temporary locals—and later restores all memory and the call stack, resuming from the same point as if the program never stopped. This resembles JavaScript's async/await, but requires no special syntax or runtime support from the language, working instead by transforming plain synchronous functions at compile time.
Taking the earlier async sleep example, Asyncify transforms the code into something like the following pseudocode:
if (mode == NORMAL_EXECUTION) {
puts("A");
async_sleep(1);
saveLocals();
mode = UNWINDING;
return;
}
if (mode == REWINDING) {
restoreLocals();
mode = NORMAL_EXECUTION;
}
puts("B");
Initially mode is NORMAL_EXECUTION. On first execution, the code runs up to async_sleep(), schedules the async operation, saves all locals, and unwinds the stack by returning from each function to the top, yielding control to the browser's event loop. When the sleep resolves, Asyncify support code sets mode to REWINDING and calls the function again. The normal execution branch is skipped this time, avoiding repeating side effects, and code reaches the rewinding branch where it restores stored locals and continues as if never interrupted.
Cost of the transformation
Asyncify isn't free. It injects substantial supporting code for storing and restoring locals and navigating the call stack in different modes. It attempts to modify only the marked asynchronous functions and their callers, but code size overhead can reach roughly 50% before compression.

This is often acceptable when the alternative is losing functionality or making substantial rewrites. Always enable optimizations for final builds, and review Asyncify-specific optimization options to limit transforms to specified functions or only direct calls. There's a minor runtime performance cost, but it's confined to the async calls themselves and typically negligible compared to the actual work performed.
Real-world demonstrations
One compelling use case is mapping WASI—the standard for WebAssembly I/O on consoles and servers—to the asynchronous File System Access API on the web. WASI exposes file system and other operations synchronously, matching the design of system languages. Mapping these to the browser's asynchronous file access lets you compile any application targeting WASI and run it in a web sandbox with real user file access.
A live demo compiles the Rust coreutils crate to WASI, applies the Asyncify transform, and implements bindings from WASI to File System Access API in JavaScript. Combined with the Xterm.js terminal component, this provides a realistic shell in a browser tab operating on real user files. Try it live at https://wasi.rreverser.com/.
Asyncify extends beyond filesystems. With similar mapping, libusb—a popular native library for USB devices—can be ported to the WebUSB API, which provides asynchronous access to USB devices. After mapping and compiling, the standard libusb tests and examples ran against selected devices inside a web page sandbox.

These examples show how Asyncify bridges the gap between synchronous Wasm applications and asynchronous web APIs, bringing cross-platform access, sandboxing, and better security to existing applications without losing functionality.



