Skip to content

Allow catching JavaScript exceptions from C++ #11496

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
RReverser opened this issue Jun 25, 2020 · 23 comments
Open

Allow catching JavaScript exceptions from C++ #11496

RReverser opened this issue Jun 25, 2020 · 23 comments
Labels

Comments

@RReverser
Copy link
Collaborator

Somewhat wider version of #11434, but applicable to synchronous functions as well: right now, even with exception handling enabled, there is no way to catch an exception from JavaScript call in C++ and extract its properties.

This means that things like DOMException can't be gracefully handled on C++ side or e.g. when integrating with C libraries where you want to convert different kinds of DOMException into different error codes.

I propose to make it possible to catch any JS exceptions and rethrow them on C++ side with Embind, so that things like

try {
  someValue.call("method");
} catch (val e) {
  // ...handle val
}

would work and return a JS handle to the caught exception.

@kripken kripken added the embind label Jun 25, 2020
@sbc100
Copy link
Collaborator

sbc100 commented Jun 25, 2020

Sorry I didn't fully read your plan.. I see that you are proposing the same thing.. deleting my first comment.

@aheejin
Copy link
Member

aheejin commented Jun 25, 2020

@RReverser Are you talking about new wasm EH proposal or the existing Emscripten EH?

@RReverser
Copy link
Collaborator Author

@aheejin Those are implementation details that are kind of orthogonal to my proposal; the proposal is mainly about catching exceptions on JS side and rethrowing them as JS value handles with Embind on C++ side. How C++ exceptions are implemented shouldn't matter in this context.

@gerald-dotcom
Copy link

I badly need this to catch errors related to Fetch API since emscripten's own Fetch API is callback based, and I prefer my own version with val's await()

I'm currently using ugly hacky workaround to catch exceptions.

@datdinhquoc
Copy link

How's the progress of this feature? I want to catch exception by JSON.parse

@RReverser
Copy link
Collaborator Author

I don't think there was any but I'm really hoping it will get picked someday. Offtop: didn't realise it's been 1.5 years since I've submitted it 🤯

@aheejin
Copy link
Member

aheejin commented Jan 31, 2022

I'm not sure what this is proposing.

In the original post,

there is no way to catch an exception from JavaScript call in C++ and extract its properties.

In #11496 (comment),

the proposal is mainly about catching exceptions on JS side and rethrowing them as JS value handles with Embind on C++ side.

So the former seems to be asking for the capability that we catch JS exceptions from C++. But the latter says "catching exceptions on JS side". Can you provide some example code?

And is this possibly related to recent discussions in #6330?

@RReverser
Copy link
Collaborator Author

RReverser commented Feb 1, 2022

So the former seems to be asking for the capability that we catch JS exceptions from C++. But the latter says "catching exceptions on JS side". Can you provide some example code?

Sorry for the confusion. The former is for what we'd want feature-wise (what it would look like for the user == catching JS exception from C++), whereas in the latter I was proposing how it could be implemented (implementation detail == catching JS exception from JS, storing it as emscripten::val and rethrowing on C++ side).

The example in issue description is still what I'd want it to look like for the user.

@RReverser
Copy link
Collaborator Author

And is this possibly related to recent discussions in #6330?

That one is important too, but is the opposite of this one. Here we discuss letting C++ to catch & parse exceptions thrown by JS, whereas #6330 is about throwing exceptions from C++ and catching & parsing them in JS.

@goldwaving
Copy link
Contributor

Having a way to catch JavaScript exceptions in C++ would be very helpful. If an unexpected JavaScript exception occurs in the program's main loop C++ thread, the thread dies and it appears that the app has frozen. Trying to explain how to use the developer console to users to get the error message is not an option. Currently I have to hack worker.js file to at least add an alert that the app has failed. I would really like a way to contain exceptions in the thread in C++ and possibly keep the thread alive.

@sbc100
Copy link
Collaborator

sbc100 commented Jun 9, 2022

Any exceptions that occurs in a pthread (worker.js) should get propagated back to the main thread and cause the application to abort. If any thread dies the whole application should die/abort. If you install an onerror handler on your main page it should trigger.

If you are not seeing that behaviour then perhaps we have a bug. Can you explain a little more about your situation.

@goldwaving
Copy link
Contributor

The problem is that there is no way to capture (and contain) some exceptions (see #16548) in the pthread. The pthead terminates, so the app dies. When working with a large file on Safari we cannot gracefully fail with an error message when Safari hits the memory limit.

@sbc100
Copy link
Collaborator

sbc100 commented Jun 13, 2022

Can you not catch the error on the main page via the onerror handler?

@goldwaving
Copy link
Contributor

Probably, but by then it is way too late. The app's main pthread is terminated. Not being able to detect a "disk full" in the thread is the problem. Because we cannot catch JS exceptions in C++, the exception cannot be contained in the thread that triggered it.

Try creating a thread with the following code:

try
{
	ofstream f("test.tmp");
	while ( 1 )
	{
		char x[100000];
		f.write(x, sizeof(x));
	}
}
catch(const std::exception& e)
{
	std::cerr << e.what();
}

The thread terminates when f.write fails and the exception is never caught within the thread.

@aheejin
Copy link
Member

aheejin commented Jun 14, 2022

As you pointed out, catching JS exceptions from C++ is not yet supported. We're planning to work on it but it may not be very soon.

Is there a way from C++ to check if a JS exception has occurred? If so, in the meantime, if what you want is to print some error messages and abort, does this workaround work?

struct SomeObject {
  ~SomeObject() {
    if (JS exception occurred) {
      Print some error message
      abort();
    }
  }
};

int main() {
  SomeObject o;
  Do something that can throw JS exceptions
}

@goldwaving
Copy link
Contributor

Unfortunately the destructor isn't called. The moment the range error occurs in f.write, the thread terminates.

@sbc100
Copy link
Collaborator

sbc100 commented Jun 14, 2022

The thing you are trying to catch is an "out of memory" during malloc, right? The FS layer may need some modification to check all it allocation site. Also you would need to make sure you build with -sABORTING_MALLOC=0 since by default malloc failures are not recoverable, regardless of what thread they occur in.

In short, trying to catch and recover from out of memory errors is non-trivial... are you sure this is something you really need? What do you plan to do in such cases?

@goldwaving
Copy link
Contributor

No. If the loop was:

while ( 1 )
{
	char *x = new char[1000000];
}

The memory error is caught and the thread continues to run. There is no problem (aside from a massive memory leak :) ).

The problem is that f.write fails in a way that cannot be caught, due to an unhandled JS exception, possibly in the FS layer as you mentioned. Having the app fail uncontrollably when trying to save data is a serious problem. If the failure could be caught, then the app could offer alternatives, such as sending the data to cloud storage or over the network.

@sbc100
Copy link
Collaborator

sbc100 commented Jun 14, 2022

Out of interest, how do you catch the failure of new char[1000000];? Are you catching bad_alloc? Are you building with -sABORTING_MALLOC=0 in order to do that?

In general do you catch all bad_allocs in your program and try to recover from them? It seems like having a top level bad_alloc handler would be mostly useless, no? For example your save data.. when you allocate the space for that (before you call fwrite) do you wrap it in a try/catch? I'm not saying we can't or shouldn't fix fwrite here, just trying to imagine where these errors are useful.

What would you think about having fwrite return ENOMEM in such cases (it seems like that is the expected failure mode for system calls and would the kind of error that most folks are already checking for when they call fwrite).

@goldwaving
Copy link
Contributor

-sABORTING_MALLOC=0 is not set, but C++ catches the allocation failure as one would expect (bad_alloc).

The problem isn't really a memory allocation error. It is a problem in the file system not catching a "disk full" exception and handling that in an appropriate way. The exception goes unhandled. So ofstream::write is not returning at all on a failure (even with exceptions enabled for the stream). The thread just terminates. Ideally ofstream::write should just set the fail bit and return when the "disk is full", but it does not. -sABORTING_MALLOC=0 makes no difference.

Having a way to handle JS exceptions in C++ would give C++ developers a way of hardening the app from such failures. A simple ofstream::write should not be able to crash the app. Of course this bug in the file system needs to be fixed too.

@aheejin
Copy link
Member

aheejin commented Jun 15, 2022

Does EM_ASM work as a temporary workaround? For example, if your top-level function is main, rename it to real_main or something, and create a wrapper function in C++

int main() {
  EM_ASM({
    try {
      real_main();
    } catch (e) {
      Handle the error gracefully
    }
  });
}

@goldwaving
Copy link
Contributor

Here is a complete sample. There is no way to get to the "Success!" message when a thread is used. Writing a large file requiring slow encoding on a thread to keep the browser responsive is a very common use scenario. In my case it is critical to keep the thread alive by containing the error within the thread. Emscripten always kills the thread in this sample.

// Build with: emcc main.cpp -o main.html -pthread -fexceptions -sPTHREAD_POOL_SIZE_STRICT=0
// Adding sABORTING_MALLOC=0 or -sDISABLE_EXCEPTION_CATCHING=0 makes no difference

#include <fstream>
#include <iostream>
#include <thread>
#include <vector>
#include <emscripten.h>

using namespace std;

extern "C" EMSCRIPTEN_KEEPALIVE void write_error()
{
	try
	{
		ofstream	f("test.tmp");
		size_t		p = 0;
		while ( 1 )
		{
			f.seekp(p);
			p += 100000000;
			f.write("", 1);
			cout << p << "\n";
		}
	}
	catch(const exception& e)
	{
		cerr << e.what() << '\n';
	}
	cout << "Success!\n";
}

void write_error2()
{
	EM_ASM(
		try
		{
			Module['_write_error']();
		}
		catch(e)
		{
			console.error(e);
		}
	);
	cout << "Success!\n";
}

thread writer;

int main()
{
	//writer = thread( write_error );		// No 'Success!'
	writer = thread( write_error2 );		// No 'Success!'
	emscripten_exit_with_live_runtime();
	return 0;
}

@sbc100
Copy link
Collaborator

sbc100 commented Jun 15, 2022

It sounds like the immediate problem will be fixed by having fwrite return an error when it cannot allocate more space.

I'm rather surprised that write_error2 doesn't do what you expect. If that doesn't work it implies the that error is not catchable by JS either... that would be very odd.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

7 participants