Skip to content
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

Ambiguity/identity loss when (re)throwing external exceptions #207

Open
dcodeIO opened this issue May 2, 2022 · 4 comments
Open

Ambiguity/identity loss when (re)throwing external exceptions #207

dcodeIO opened this issue May 2, 2022 · 4 comments

Comments

@dcodeIO
Copy link

dcodeIO commented May 2, 2022

To handle an external exception, for example originating in JavaScript, one would naturally use a catch_all clause iiuc. The caught exception can (only) be rethrown unmodified as long as it does not escape the scope of the catch clause, implying some limitations. To give an example of what I mean, imagine the following pseudocode in Wasm with EH:

try {
  maybeThrowExternalException();
} catch (Exception ex) {
  // ...
}

where ex cannot refer to the original exception since there is no such reference available when using a catch_all clause, so a compiler will likely utilize a substitute, say an ExternalException instance to represent the exception. When raising the exception again, this leads to an ambiguity within the catch clause if a language does not itself have an expression-less throw; instruction (JS for example considers this a syntax error) or likewise to indicate the intent to rethrow:

try {
  maybeThrowExternalException();
} catch (Exception ex) {
  throw ex; // rethrow the original or throw the substitute? what if ex is modified?
}

Furthermore, if the exception escapes the catch clause, the only available option becomes to throw the substitute, with the exception losing identity and being unable to refer to the original exception due the unavailable reference:

Exception escapedException;
try {
  maybeThrowExternalException();
} catch (Exception ex) {
  escapedException = ex;
}
if (escapedException) throw ex; // throw the substitute

Now I know that exnref has been removed for reasons, yet being able to reference an (external) exception in catch_all seems useful. Externally in JS this is not a problem because there is such a reference, a WebAssembly.Exception. Could there perhaps be a similar reference in Wasm when using/limited to a catch_all clause? Or are there clever tricks to emulate the desired behavior in a portable way?

Related: #202

@dschuff
Copy link
Member

dschuff commented May 2, 2022

I'm not sure I'm clear on what exactly the use case is here.
For example if you want to understand or modify the external exception in any way, I don't see any way to avoid importing the exception type (in which case you don't need to use catch_all at all, you can just catch that type explicitly). I think we do also want to expose the "JavaScript exception tag" (i.e. #202) so that wasm can get the identity of such exceptions as an externref.

Doing one of those things will allow a toolchain to track exception identity explicitly either internally to the language (e.g. as C++ has an exception object allocated in linear memory), or externally via an externref which corresponds to a JS exception object (or externally via importing some other language or module's definition of an exception).

In emscripten the current plan is to actually do both; this is because we use the C++ standard way of allocating exception objects, but also because we want to have the option of preserving exception identity via a JS object so that we can attach a JS stack trace to it. The idea is that we can call to JS to allocate a WebAssembly.Exception attach the stack trace, and throw that; and then that object can be rethrown either implictly or explicitly, and if it's eventually unhandled it will propagate out to JS with the stack trace still attached, as JS Error objects do.

@dcodeIO
Copy link
Author

dcodeIO commented May 3, 2022

The use case I am currently thinking about is ~ the following: When a try statement is compiled, it might become

(local $exception i32)
;; code before
block $try
 try
  ;; code of try block
  br $try
 catch $SomeTag
  local.set $exception
 catch_all
  ;; make substitute
  local.set $exception
 end
 ;; code of catch
end
;; code after

so that any kind of exception can be handled according to the expectation that a source level

// code before
try {
  // code of try block
} catch (Exception exception) {
  // code of catch
}
// code after

is guaranteed to handle any exception, known or not, where exception is (and can only be) a substitute when the catch_all clause is triggered. Here, exceptions the language can handle are guaranteed to be some subclass of Exception, so the substitute is a subclass of Exception as well, say an ExternalException <: Exception.

Now, #202 addresses the case where the unknown exception is a JS exception, which can be modeled by importing JS's exception tag as envisioned and providing, say, an immutable JSException <: Exception containing the JS object to throw on a subsequent throw of the JSException. This approach, however, is non-portable to other kinds of unknown exceptions.

As such, what I am particularly interested in is the general case, for example when modules are (re-)composed post-compilation. Modules can be moved between hosts for example, or various languages can be involved that are not necessarily aware of each other at compilation time so there are no tags to import, yet there may be the need to defensively handle any exception, leading to either a) loss of identity via substitute or b) the need for a non-portable, non-standard workaround, which both seem desirable to avoid - for example by providing a usable reference in the general case.

@dschuff
Copy link
Member

dschuff commented May 3, 2022

OK, I think I understand what you're getting at. I think what you've shown is what you'd do if you want to actually handle an unknown exception (as opposed to, say, running some local cleanup and then rethrowing it).
I guess what I don't quite understand is why the identity matters in that case. Since the exception is truly foreign and nothing can be known about the exception or its payload, what does its identity mean? If the exception isn't going to be rethrown, what purpose does it serve for the local function to store the identity? In the general case, the local function can't even be sure it's safe to handle the exception, e.g. if it's situated in the call stack between the thrower and an expected catcher, as the exception may not even represent an error.
Also I don't know if a general reference would make sense in the general case, since an embedding may not support reference types and AFAIK strictly speaking, (outside of the JS embedding) exceptions do not actually need to have an identity that is meaningful outside of a try scope.

@dcodeIO
Copy link
Author

dcodeIO commented May 4, 2022

It's true that the identity does not necessarily matter for the cleanup code in an intermediary module (i.e. a substitute might be fine), but preserving identity might nonetheless matter for the original caller that does not expect that a substitute became necessary half-way. What I imagine is that, depending on how complex the cleanup code is, and where or when within the cleanup code the original exception is thrown for the caller again (say cleanup code is async [e.g. goes through a callback] and/or the rethrow happens in another function, which is fine in any language that assumes that any exception is referenceable), there might not be the option to use a rethrow instruction in the initial catch_all, which however is the only way to preserve identity. I imagine that just having access to a (possibly virtual) reference, be it only to refer back to the original exception in order to preserve its identity, would be useful.

As a reference point, I guess an incomplete solution mid-way between a virtual reference and a properly typed reference (e.g. imported JS exception tag) can be imagined as a generalization of #202 that works for any kind of exception that can be represented by an externref (aka anyref) (JS Error, Java Exception, C# Exception, ...), yet being even more general seems desirable so it works in any case.

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

No branches or pull requests

2 participants