JS catch wasm out of memory?

Is there a way to register a handler of some form for JS to catch when a wasm module (index.html or webworker) has run out of memory and crashed ?

[The alternative is to send a heartbeat every second, but in that case it is not obvious how to tell if a webworker is busy crunching data or if it has crashed].

My understanding is that if a WASM module running in a JS host crashes, then a WebAssembly.RuntimeError exception is thrown. You should be able to catch that exception.

2 Likes
  1. This looks legit.

  2. I am building my wasm32 with: cargo build --target=wasm32-unknown-unknown which, among other things, generates a *.js file which ends with:

async function init(input) {
    if (typeof input === 'undefined') {
        input = new URL('client_w_bg.wasm', import.meta.url);
    }
    const imports = getImports();
                                         
    if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
        input = fetch(input);                 
    }
                              
    initMemory(imports);                        
                                                                    
    const { instance, module } = await load(await input, imports);
                                
    return finalizeInit(instance, module);
}
                                          
export { initSync }
export default init;        

any intuition on whether this is achievable by wrapping around the existing tools, or if this would likely require modifying an auto generated *.js file?

I don't really understand the code snippet you posted here very well, but I would consider it a bug if the generated JS doesn't allow you to catch the exception in question.

I recommend that you try it. Write some Rust code that immediately fails, e.g.

let mut data = Vec::new();
data.resize(1000000000000, 42u8);

Then see what happens. My guess is that the location where you called the generated JS file will throw the exception I mentioned.

2 Likes

strawmanning

rustc is smart:

error: literal out of range for `usize`
   |
79 |         data.resize(10_000_000_000, 42_u8);
   |                     ^^^^^^^^^^^^^^
   |
   = note: `#[deny(overflowing_literals)]` on by default
   = note: the literal `10_000_000_000` does not fit into the type `usize` whose range is `0..=4294967295`

steelmanning

        pub struct T {
            data: [u8; 100],}

        let mut data = Vec::<T>::new();

        data.reserve(1_000_000_000);

Here are the two errors I am getting in Chrome dev console right now:


client_w.js:474 panicked at 'capacity overflow', library/alloc/src/raw_vec.rs:517:5

Stack:

Error
    at imports.wbg.__wbg_new_693216e109162396 (http://localhost:8080/client_w.js:477:17)
    at console_error_panic_hook::Error::new::h8668ab84cb67189b (http://localhost:8080/client_w_bg.wasm:wasm-function[15376]:0x41a242)
    at console_error_panic_hook::hook_impl::hbfeb05050ef0e2aa (http://localhost:8080/client_w_bg.wasm:wasm-function[1791]:0x242799)
    at console_error_panic_hook::hook::h6a9927c1f0888f41 (http://localhost:8080/client_w_bg.wasm:wasm-function[16491]:0x429d6d)
    at core::ops::function::Fn::call::h167ccd9c6a07f6c8 (http://localhost:8080/client_w_bg.wasm:wasm-function[14229]:0x408382)
    at std::panicking::rust_panic_with_hook::h1c368a27f9b0afe1 (http://localhost:8080/client_w_bg.wasm:wasm-function[3903]:0x2e23f7)
    at std::panicking::begin_panic_handler::{{closure}}::h8e1f8b682ca33009 (http://localhost:8080/client_w_bg.wasm:wasm-function[6135]:0x348636)
    at std::sys_common::backtrace::__rust_end_short_backtrace::h7f7da41799766719 (http://localhost:8080/client_w_bg.wasm:wasm-function[19727]:0x44a9f1)
    at rust_begin_unwind (http://localhost:8080/client_w_bg.wasm:wasm-function[15646]:0x41e18f)
    at core::panicking::panic_fmt::hcdb13a4b2416cf82 (http://localhost:8080/client_w_bg.wasm:wasm-function[16345]:0x427de3)

client_w_bg.wasm:0x44b77b Uncaught (in promise) RuntimeError: unreachable
    at __rust_start_panic (client_w_bg.wasm:0x44b77b)
    at rust_panic (client_w_bg.wasm:0x43fc20)
    at std::panicking::rust_panic_with_hook::h1c368a27f9b0afe1 (client_w_bg.wasm:0x2e2424)
    at std::panicking::begin_panic_handler::{{closure}}::h8e1f8b682ca33009 (client_w_bg.wasm:0x348636)
    at std::sys_common::backtrace::__rust_end_short_backtrace::h7f7da41799766719 (client_w_bg.wasm:0x44a9f1)
    at rust_begin_unwind (client_w_bg.wasm:0x41e18f)
    at core::panicking::panic_fmt::hcdb13a4b2416cf82 (client_w_bg.wasm:0x427de3)
    at alloc::raw_vec::capacity_overflow::h9e24c4d5bbf80bc3 (client_w_bg.wasm:0x427da3)
    at alloc::raw_vec::handle_reserve::h3bf48243ce6ec1b2 (client_w_bg.wasm:0x319568)
    at alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle::h96b1e49ef4ba0aad (client_w_bg.wasm:0x39431a)

analysis

The first one "panicked at 'capacity overflow'" definitely looks like a Rust exception.

I am not clear if the second "Uncaught (in promis) RuntimeError: unreachable" is a Rust or a JS exception.

For comparison, if I just type
throw "blah" into the Chrome dev console, the response is:

VM304:1 Uncaught blah
(anonymous) @ VM304:1

failing to catch it in JS land

I have the following JS fragment:

import init, * as wasm from "./client_w.js";
try {
        await init();
        var t2 = await wasm.Rust_Util.start_th_router()
    } catch (e) {
        console.log("JS caught error: ", e);
    }

init is the JS function show in previous post, generated by the cargo build process

RustUtil is a Rust struct that looks like:

#[wasm_bindgen]
impl Rust_Util {
    pub async fn start_th_router() {
        Th_Router::start().await}
...
}

(Th_Router::start ends up doing the massive Vec).

I'm not sure why JS is not catching the exception right now. Possibly something to do with async (pure guess, no hard evidence).

Unfortunately, if the exception isn't available to be caught, then I do not know why.

I note that there's a difference between the following two ways of failing:

  1. Overflow when computing the size to pass to the allocator.
  2. The allocator is actually called, but returns a null pointer.

On platforms I am familiar with, the first option results in a panic, and the second option results in an unconditional abort. Supposedly panics always abort on WASM, so maybe they're indistinguishable from JS, but maybe they aren't.

There's something else that gives me hope. The full error (which I failed to post last time since it involves clicking this little triangle error to expand) is:

client_w_bg.wasm:0x44b77b Uncaught (in promise) RuntimeError: unreachable
    at __rust_start_panic (client_w_bg.wasm:0x44b77b)
    at rust_panic (client_w_bg.wasm:0x43fc20)
    at std::panicking::rust_panic_with_hook::h1c368a27f9b0afe1 (client_w_bg.wasm:0x2e2424)
    at std::panicking::begin_panic_handler::{{closure}}::h8e1f8b682ca33009 (client_w_bg.wasm:0x348636)
    at std::sys_common::backtrace::__rust_end_short_backtrace::h7f7da41799766719 (client_w_bg.wasm:0x44a9f1)
    at rust_begin_unwind (client_w_bg.wasm:0x41e18f)
    at core::panicking::panic_fmt::hcdb13a4b2416cf82 (client_w_bg.wasm:0x427de3)
    at alloc::raw_vec::capacity_overflow::h9e24c4d5bbf80bc3 (client_w_bg.wasm:0x427da3)
    at alloc::raw_vec::handle_reserve::h3bf48243ce6ec1b2 (client_w_bg.wasm:0x319568)
    at alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle::h96b1e49ef4ba0aad (client_w_bg.wasm:0x39431a)

followed by the following part, which Chrome seems to not want to paste:

$__rust_start_panic	@	client_w_bg.wasm:0x44b77b
$rust_panic	@	client_w_bg.wasm:0x43fc20
$std::panicking::rust_panic_with_hook::h1c368a27f9b0afe1	@	client_w_bg.wasm:0x2e2424
$std::panicking::begin_panic_handler::{{closure}}::h8e1f8b682ca33009	@	client_w_bg.wasm:0x348636
$std::sys_common::backtrace::__rust_end_short_backtrace::h7f7da41799766719	@	client_w_bg.wasm:0x44a9f1
$rust_begin_unwind	@	client_w_bg.wasm:0x41e18f
$core::panicking::panic_fmt::hcdb13a4b2416cf82	@	client_w_bg.wasm:0x427de3
$alloc::raw_vec::capacity_overflow::h9e24c4d5bbf80bc3	@	client_w_bg.wasm:0x427da3
$alloc::raw_vec::handle_reserve::h3bf48243ce6ec1b2	@	client_w_bg.wasm:0x319568
$alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle::h96b1e49ef4ba0aad	@	client_w_bg.wasm:0x39431a
$alloc::raw_vec::RawVec<T,A>::reserve::ha4813c8fa6d0fe56	@	client_w_bg.wasm:0x39b9e8
$alloc::vec::Vec<T,A>::reserve::h8329cc9179c57cad	@	client_w_bg.wasm:0x405f32
$x_client::th::th_router::th_router::<impl x_client::th::th_router::Th_Router>::start::{{closure}}::ha175e77530371d60	@	client_w_bg.wasm:0x1a83cb
$<core::future::from_generator::GenFuture<T> as core::future::future::Future>::poll::hb15a1e5345c1f3de	@	client_w_bg.wasm:0x31a1c6
$x_client::rust_util::Rust_Util::start_th_router::{{closure}}::h4387bae8d9a5b7f8	@	client_w_bg.wasm:0x227955
$<core::future::from_generator::GenFuture<T> as core::future::future::Future>::poll::hb681e8281e88abfa	@	client_w_bg.wasm:0x31a286
$x_client::rust_util::Rust_Util::start_th_router::{{closure}}::__wasm_bindgen_generated_Rust_Util_start_th_router__const::__wasm_bindgen_generated_Rust_Util_start_th_router::{{closure}}::h0052c0a885e5f1d2	@	client_w_bg.wasm:0x219492
$<core::future::from_generator::GenFuture<T> as core::future::future::Future>::poll::h06357a8143aaa79b	@	client_w_bg.wasm:0x2df111
$wasm_bindgen_futures::future_to_promise::{{closure}}::{{closure}}::hac35c2ccc3cc4680	@	client_w_bg.wasm:0x192e47
$<core::future::from_generator::GenFuture<T> as core::future::future::Future>::poll::h8f7a545112f71a57	@	client_w_bg.wasm:0x31a106
$wasm_bindgen_futures::task::singlethread::Task::run::h866c3547ae45c265	@	client_w_bg.wasm:0x211bd6
$wasm_bindgen_futures::queue::QueueState::run_all::h0df0ec7a3376dcdc	@	client_w_bg.wasm:0x1e4b11
$wasm_bindgen_futures::queue::Queue::new::{{closure}}::h4b48710e818f623b	@	client_w_bg.wasm:0x39caf4
$<dyn core::ops::function::FnMut<(A,)>+Output = R as wasm_bindgen::closure::WasmClosure>::describe::invoke::he4219956c4b8baba	@	client_w_bg.wasm:0x2fb75f
__wbg_adapter_22	@	client_w.js:217
real	@	client_w.js:202
Promise.then (async)		
imports.wbg.__wbg_then_18da6e5453572fc8	@	client_w.js:576
$js_sys::Promise::then::ha706cc8b497d9959	@	client_w_bg.wasm:0x38fe61
$wasm_bindgen_futures::queue::Queue::schedule_task::hb37c5e080a99d48f	@	client_w_bg.wasm:0x26e36b
$wasm_bindgen_futures::task::singlethread::Task::spawn::{{closure}}::hf1401b79ff985fed	@	client_w_bg.wasm:0x41a3d0
$std::thread::local::LocalKey<T>::try_with::h6d78ed4a0df90a96	@	client_w_bg.wasm:0x2bfa5f
$std::thread::local::LocalKey<T>::with::h88e2d99c5d3190c6	@	client_w_bg.wasm:0x385e3e
$wasm_bindgen_futures::task::singlethread::Task::spawn::h15abdf1670060700	@	client_w_bg.wasm:0x1c7751
$wasm_bindgen_futures::spawn_local::hd875770121d54161	@	client_w_bg.wasm:0x2bb107
$wasm_bindgen_futures::future_to_promise::{{closure}}::hb32ae5b744dbef9c	@	client_w_bg.wasm:0x2929f3
$wasm_bindgen::convert::closures::invoke2_mut::he84dc53740f6e38c	@	client_w_bg.wasm:0x2e211f
__wbg_adapter_79	@	client_w.js:282
cb0	@	client_w.js:560
imports.wbg.__wbg_new_52205195aa880fc2	@	client_w.js:565
$js_sys::Promise::new::h33abaea97ab56066	@	client_w_bg.wasm:0x37f847
$wasm_bindgen_futures::future_to_promise::h470c299cfcd8cbff	@	client_w_bg.wasm:0x2d687d
$rust_util_start_th_router	@	client_w_bg.wasm:0x2dd286
start_th_router	@	client_w.js:362
(anonymous)	@	(index):27

Now, if we look at the last two lines ,we see:

start_th_router	@	client_w.js:362
(anonymous)	@	(index):27

(index) here refers to index.html

client_w.js

    /**
    * @returns {Promise<void>}
    */
    static start_th_router() {
        const ret = wasm.rust_util_start_th_router(); // ### LINE 362 ###
         return takeObject(ret);
    }

index.html

    try {
        await init();
        var t2 = await wasm.Rust_Util.start_th_router() // ### line 27 ###
    } catch (e) {
        console.log("JS caught error: ", e);
    }

analysis

So we have identified how the calls are happening; the thrown exceptions even agrees with us in the call stack, but for whatever reason, we are not (yet) catching the exception.

This is the result of calling std::process::abort() (which on wasm codegens to the unreachable instruction), which Rust does on panics in case of -Cpanic=abort.

Here are things I do not understand:

  1. Is this Rust/wasm panic being converted to a JS exception ?

  2. If yes, why can't I catch it.

  3. If no, why is it bright red in the Chrome dev console.

  4. Furthermore, because this is a Rust panic, does it mean we don't actually hit the wasm 4GB limit? I.e. is the allocator in generated Rust/wasm seeing that it is about to hit the 4GB limit -- and then deciding to abort before actually hitting the 4GB limit ? This might explain why this is a Rust panic/abort rather than a JS/Wasm error.

If #4 is true, it probably means I should be hunting wasm_bindgen docs for ways to force all Rust abort/panics to be converted to JS exceptions ?

Wasm doesn't have exceptions. The Rust panic is converted into an abort using the wasm unreachable instruction. This instruction is defined in the JS Wasm api to result in a RuntimeError: unreachable Javascript exception without any method for wasm to catch the exception.

The exception doesn't directly occur in start_th_router function call, but in a Javascript Promise spawned from it. I believe it should result in a Result::Err on the rust side. I don't know what other code you have, but if you ignore the Result, the exception will be ignored.

The grow_memory instruction used for growing the heap may return an error in OOM cases. Rust catches this and prints an error if this happens before aborting. In your specific case I think it was a capacity overflow (which basically means attempting to allocate more than fits in a usize (or isize?)) rather than a failed memory allocation though.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.