How to Fix: Wasmtime does not exit with the same code as the wasm main on Windows
Wasmtime on Windows can report the wrong process exit code when a WASI WebAssembly program returns a value greater than 1 from main. The result is confusing in CI, shell scripts, and test harnesses because the wasm module exits correctly internally, but the outer wasmtime.exe process does not always propagate that same status code on Windows.
Problem Overview
This issue shows up when you compile or run a WebAssembly program whose main function returns a non-zero exit code, especially a value greater than 1, and then launch it with Wasmtime on Windows. On Unix-like systems, the exit code often behaves as expected, but on Windows the final process status observed by cmd, PowerShell, or calling automation may differ from the value returned inside the module.
That mismatch matters because many build systems rely on exact exit codes to distinguish between categories of failure. If Wasmtime collapses or rewrites the exit status, your automation can incorrectly interpret a test failure, runtime failure, or application-defined status.
Understanding the Root Cause
The root of the bug is the boundary between WASI exit semantics and Windows process exit semantics. Inside a WASI program, a return from main or a call to proc_exit becomes a structured runtime event. Wasmtime then has to translate that event into the actual host process exit code of wasmtime.exe.
On Windows, that translation can be affected by how the runtime maps errors, traps, and explicit process termination. In practice, the bug appears when Wasmtime handles the WASI termination path differently from the normal host process exit path, so the exit code observed by the parent shell is not the original application exit code.
Technically, there are three layers involved:
- The wasm program returns a status from main or invokes proc_exit(code).
- Wasmtime’s WASI integration converts that into an internal termination result.
- The Windows host process must terminate with the same integer status via the platform-specific process exit mechanism.
If any part of that chain treats the result as a generic runtime error instead of an explicit WASI exit, the final code can be normalized, truncated, or replaced. That is why the bug is typically platform-specific and easiest to observe in shell-based reproduction steps.
Step-by-Step Solution
The practical fix depends on whether you are using Wasmtime as a CLI tool, embedding Wasmtime, or working around the issue until an upstream fix is available.
1. Reproduce the bug reliably
Start with a tiny WASI program that exits with a value greater than 1.
#include <stdlib.h>
int main(void) {
return 7;
}
Compile it to a WASI target using your preferred toolchain.
clang --target=wasm32-wasi -O2 -o exit7.wasm exit7.c
Run it with Wasmtime on Windows.
wasmtime exit7.wasm
echo %ERRORLEVEL%
Or in PowerShell:
wasmtime exit7.wasm
$LASTEXITCODE
If the issue is present, the value printed by the shell will not match 7.
2. Prefer explicit WASI exit in test cases
If your goal is to validate runtime behavior precisely, use an explicit proc_exit path instead of relying only on the C runtime returning from main. This helps distinguish between a compiler/runtime startup issue and a Wasmtime process-exit propagation issue.
#include <wasi/api.h>
int main(void) {
__wasi_proc_exit(7);
return 0;
}
Compile and run again:
clang --target=wasm32-wasi -O2 -o proc_exit7.wasm proc_exit7.c
wasmtime proc_exit7.wasm
If both the return 7 and proc_exit(7) forms produce the wrong Windows exit status, the problem is clearly in host-side propagation.
3. Validate with a native wrapper if you need stable automation
Until the runtime version you use includes a confirmed fix, one reliable workaround is to avoid depending on wasmtime.exe as the final authority for CI exit-code checks on Windows. Instead, create a small wrapper that inspects Wasmtime’s result more directly when embedding, or normalize behavior in your test harness.
For example, if you are embedding Wasmtime in Rust, make sure you handle the explicit WASI exit case and terminate the host process with that exact code.
use std::process;
fn main() {
let exit_code = run_wasm_and_capture_wasi_exit();
process::exit(exit_code);
}
fn run_wasm_and_capture_wasi_exit() -> i32 {
7
}
The key idea is simple: capture the WASI exit status as data, then call the host platform’s process exit API with the same integer.
4. If you maintain Wasmtime-based tooling, patch the exit propagation path
If you control the layer that launches Wasmtime or you are contributing a fix upstream, ensure the code path that handles WASI proc_exit and main-return termination does not fall through to generic error handling on Windows.
The correct behavior is:
- Detect an explicit WASI exit status.
- Extract the original exit code without rewriting it.
- Terminate the host process with that same code.
Pseudocode for the expected logic looks like this:
result = run_wasi_module()
if result is WasiExit(code):
host_process_exit(code)
else if result is Trap(err):
print_error(err)
host_process_exit(1)
else:
host_process_exit(0)
The important distinction is that WASI exit is not a generic trap. It is a structured program termination signal and should preserve the module’s intended status code exactly.
5. Verify the fix across shells
After updating Wasmtime or applying a workaround, test in both cmd.exe and PowerShell. They expose process status differently, and shell behavior can make debugging harder if you only check one environment.
cmd /c "wasmtime exit7.wasm & echo %ERRORLEVEL%"
powershell -Command "wasmtime exit7.wasm; $LASTEXITCODE"
Also validate in your CI runner, because wrappers such as GitHub Actions, Ninja, CTest, and custom runners may have their own assumptions about non-zero statuses.
Common Edge Cases
- Returning from main vs calling proc_exit: Some toolchains lower these through different runtime paths before they reach WASI. Test both.
- Trap vs exit confusion: A real runtime trap should usually become a generic failure code, but a normal WASI exit should preserve the original integer.
- Shell differences on Windows: %ERRORLEVEL% in cmd and $LASTEXITCODE in PowerShell can be checked at different times. Make sure no intermediate command resets them.
- Signed vs unsigned interpretation: Large exit codes may be represented differently depending on language bindings or host APIs. Stay within normal process-exit ranges when testing.
- CI abstraction layers: Some test runners only care whether the exit code is zero or non-zero, masking the fact that the exact code is wrong.
- Version-specific behavior: Different Wasmtime releases can change CLI exit handling, WASI support, or Windows-specific process propagation logic. Always confirm the exact version you are using.
FAQ
Why does this mostly show up on Windows?
Because the bug sits in the platform-specific handoff between WASI termination and the host process exit API. Unix and Windows do not handle that boundary identically, so a mistake can appear on one platform but not the other.
Is this a WebAssembly bug or a Wasmtime bug?
This is primarily a runtime integration bug, not a WebAssembly language bug. The wasm program is returning a valid exit status, but the host runtime process is not always propagating it correctly.
What is the safest workaround for automated tests?
If exact exit values matter, either embed Wasmtime and explicitly map WASI exit to host exit or add a Windows-specific test wrapper until your Wasmtime version includes a fix. Also verify results in the same shell and CI environment used in production.
For long-term stability, the right fix is to ensure Wasmtime treats WASI exit codes as first-class process termination values on Windows rather than as generic runtime failures. Once that mapping is corrected, a wasm program that returns 7 should cause wasmtime.exe to exit with 7 as well.