WebAssembly / exception-handling

Proposal to add exception handling to WebAssembly
https://webassembly.github.io/exception-handling/
Other
162 stars 36 forks source link

unreachable catch clause #339

Open SoniEx2 opened 6 days ago

SoniEx2 commented 6 days ago

we would like to see an "unreachable" catch clause, that takes an exception type, or even "unreachable_all"?

jumping over an "unreachable" is a bit unwieldy today:

block $a
  block $b
    try_table (catch_all $b)
      ;; code goes here
    end
    br $a
  end
  unreachable
end

meanwhile,

try_table (unreachable_all)
  ;; code goes here
end

and implementations could make this "zero-cost" in the happy case. they could even provide more information about the error than with the catch_all approach - they could provide the full stack trace of the exception instead of the stack trace of the unreachable.

(if we could call these "checked exceptions" that would be cool too, even if they're not java-style checked exceptions)

rossberg commented 6 days ago

That seems to buy only minor convenience for a very cold usage pattern. I don't think that justifies complicating the language? We don't provide such shorthands for other, similar cases of branching instructions either (br_table, br_if), where it likely is much more common. In practice, the generated machine code would probably be similar anyway.

I am also curious about the intention of this use case — what do you gain by turning uncaught exceptions into traps? Usually, people ask for the other direction. :)

bjorn3 commented 6 days ago

There are cases where you can't safely unwind past a stack frame as invariants are broken. For example if you unwind from Rust into C, rustc will abort as most C code is not safe to unwind through and even C code that is, is often not compiled with the right compiler flag to safely unwind through. Rustc wouldn't benefit from an unreachable catch clause though as it wants to run some code before aborting to print a panic message.

SoniEx2 commented 6 days ago

rustc wouldn't benefit, yeah.


we've been hand-writing wasm lately. wasm, as a programming language, has a specific quirk that works particularly well with this pattern: catching an exception always targets an outer block.

if we call this pattern the "checked exception" pattern (not to be confused with java's checked exceptions), then we can use exceptions as control flow without worrying about accidentally catching the wrong exception in the wrong place.

for example, python uses an exception (StopIteration) to signal the end of an iterator, generator, among other constructs. python had a problem where generator code could accidentally raise StopIteration, silently killing the entire iteration and causing a difficult to debug problem. their solution? effectively, since PEP 479, all generators are wrapped in something akin to

try_table (unreachable $StopIteration)
  ;; code goes here
end
throw $StopIteration

(in practice it wraps StopIteration into RuntimeError, but you're not really supposed to catch RuntimeError so it more or less works out)

while if you want to re-throw something from an inner block, well, wasm makes it trivially easy:

block $rethrow (result exnref)
  try_table (unreachable $Foo)
    call $should_not_throw_Foo
    try_table (catch_ref $Foo $rethrow)
      call $allowed_to_throw_Foo
    end
  end
  return
end
throw_ref

this is particularly important in the context of callbacks, if you want to control which exceptions from which callbacks re-appear in your API contract. you can say things like "raises Foo if callback foo raises Foo" without having to worry about callback bar raising Foo.

anyway just a thought. this is definitely a better approach to failure handling than Rust's Result (or more specifically, how you need to put your error types into a new error type so you can put it into a new Error in your function's Result), and avoids the pitfalls traditionally associated with java exceptions.

SoniEx2 commented 5 days ago

a throw_func (or throw_at) (br_throw? return_throw?) would be nice too, it's a bit awkward having to do the rethrow dance for everything.

ultimately this is like capabilities for exceptions.