emscripten-core / emscripten

Emscripten: An LLVM-to-WebAssembly Compiler
Other
25.72k stars 3.3k forks source link

Looking for a way to report unhandled c++ exceptions to 3rd party crash reporting system #22613

Open kurrak opened 2 weeks ago

kurrak commented 2 weeks ago

This is a question, not a bug.

We're trying to find a generic way to report stack traces for all exceptions unhandled in c++ (meaning: also for c++ exceptions that are handled in JS). We're using -fwasm-exceptions and -sEXCEPTION_STACK_TRACES.

We found out that Emscripten's ___throw_exception_with_stack_trace JS function is called whenever c++ exception is thrown. What we'd like to do is plug into this function, so we can get a stack trace and send it to 3rd party crash reporting system.

We've been trying different approaches, but had little to no success. We're looking for any guidance, or the reason for why it is bad/wrong idea in the first place.

Version of emscripten/emsdk:

emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.61 (67fa4c16496b157a7fc3377afd69ee0445e8a6e3)
clang version 19.0.0git (https:/github.com/llvm/llvm-project 7cfffe74eeb68fbb3fb9706ac7071f8caeeb6520)
Target: wasm32-unknown-emscripten
Thread model: posix
InstalledDir: /Users/kurak/Developer/emsdk/upstream/bin
sbc100 commented 2 weeks ago

IIUC the way this is normally done on the web is to install a global exception handler on your page and report all unhandled exceptions that way. Exceptions thrown from native code (when using EXCEPTION_STACK_TRACES) should have stack traces attacked just like Error's thrown by your JS code.

I think the window.onerror handler is the way to install a global error handler (at least on the web).

@aheejin might know more here.

kurrak commented 2 weeks ago

@sbc100 thanks for reply and thoughts. Yes, we're working on web solution.

The problem with window.onerror handler is that it is not called for errors that are handled in JS. We're trying to collect stack traces for all c++ exceptions not handled in c++. This is important, because we cannot guarantee that JS clients of our c++ API are never catching errors somewhere in call stack that in the end reaches out to c++.

To be honest, plugging into ___throw_exception_with_stack_trace is not great solution either, as it is also called when c++ exception is handled in c++ (we wouldn't like to report such events). I'd love to know if there is any way to distinguish these two scenarios, too.

Side note: window.onerror is also not called for errors that are thrown from c++ code called from within JS promise (in this case browser logs error starting with Uncaught (in promise) message), which translates to JS promise rejection. This can be easly handled by additional window.onUnhandledRejection, though.

sbc100 commented 2 weeks ago

So in addition to onerror + onUnhandledRejection you would like to know when one of your users catches and exception generated by your code? This sounds like a somewhat unusual request, but there are probably ways you can me it work.

One way to do this would be to ask you users to call your exception reporting function whenever they catch and ignore exceptions from your code.

Another way would be something like ABORT_ON_WASM_EXCEPTIONS which wraps ever wasm export in a try catch. See https://github.com/emscripten-core/emscripten/blob/543993ecf6fbf1c97c2ed56d062dc504978962c3/src/preamble.js#L525-L602. This approach does have runtime cost as well as code size cost though.

aheejin commented 2 weeks ago

I'm not sure if I understood your request correctly. When -sEXCEPTION_STACK_TRACES (or -sASSERIONS or -O0, both of which set -sEXCEPTION_STACK_TRACES) is set, all native exceptions thrown in C++ call out to JS ___throw_exception_with_stack_trace to get the stack traces embedded in the object, as you've observed. At this point we don't know where or whether exception is going to be handled. I'm not sure what you mean by "plugging into" ___throw_exception_with_stack_trace.

And whenever you have that exception object in JS, you can access its WebAssebly.stack property to get the stack trace string. It can be any of the handlers you mentioned.

I guess I don't understand what the problem is correctly. Can you elaborate more?

kurrak commented 2 weeks ago

@aheejin, @sbc100 thanks for replies 🙌

I guess I don't understand what the problem is correctly. Can you elaborate more?

Sure! Here's the thing:

One way to do this would be to ask you users to call your exception reporting function whenever they catch and ignore exceptions from your code.

This is specifically something I try to avoid.

Another way would be something like ABORT_ON_WASM_EXCEPTIONS which wraps ever wasm export in a try catch. See

ABORT_ON_WASM_EXCEPTIONS sounds like a behaviour I want: to treat c++ exception not handled in c++ similarly to c++ crash. What I was missing before was the customisation point that I can plug my code to send proper stack trace to crash reporting system (window.onerror and window.onunhandledrejection events are not an option, because they can be easily not called whenever client code catches exception anywhere along throwing call stack). Now I see there is Module.onAbort callback that seems exactly like what I need.

The challenge now is to get correct stack trace as onAbort is called differenty depending on scenario:

Do you see a good/correct way to distinguish these two scenarios in onAbort callback so I can decide how to get correct call stack? Do you know there are other ways onAbort can be called that I should handle differently?

As a side note: do you think onAbort should receive WebAssembly.Exception for scenario with not handled c++ exception?

sbc100 commented 2 weeks ago

So just to be clear, if I'm a user of your library, and your library crashes as part of my application, you want the crash report to go to your server by default? Wouldn't that be something that I, as the user of the library, would want to opt into explictly?

sbc100 commented 2 weeks ago

So just to be clear, if I'm a user of your library, and your library crashes as part of my application, you want the crash report to go to your server by default? Wouldn't that be something that I, as the user of the library, would want to opt into explictly?

Or is this actually a normal/common thing to do in the world of JS libraries? (I'm not super familiar with the latest JS library trends).

kurrak commented 2 weeks ago

So just to be clear, if I'm a user of your library, and your library crashes as part of my application, you want the crash report to go to your server by default? Wouldn't that be something that I, as the user of the library, would want to opt into explictly? Or is this actually a normal/common thing to do in the world of JS libraries? (I'm not super familiar with the latest JS library trends).

To be specific: this is internal library used by multiple client apps within our organisation. That being said this behaviour is opt-in and available behind a setting passed during library setup. I want to keep it this way: one time configuration and no more responsibility on client app side for this to work.

kurrak commented 2 weeks ago

@sbc100 our current idea for getting the access to correct call stack is to use --post-js script to override Emscripten-generated abort function. We'd wrap it in additional try/catch, get the call stack in catch block and rethrow it. We think this should work ok nevertheless of aborting scenario (crash vs. unhandled exception). Do you think it is a good approach?

oatgnauh commented 2 days ago

there is a try .. catch in side handleMessage of file worker.mjs, you may want to process with the exception throw