feat: Implement WebAssembly exception handling (try-catch-finally) #2965
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Fixes #302.
Related: #2956, #484, #447.
Motivation
The WebAssembly exception handling proposal reached Phase 4 and shipped in V8 9.5 (Chrome 95, October 2021), Firefox 100 (May 2022), and Safari 15.2 (December 2021). All current Node.js LTS versions (18, 20, 22, 24) include V8 engines well past 9.5, meaning exception handling is available without flags in any supported runtime. This implementation brings AssemblyScript in line with these mature runtime capabilities.
This implementation is entirely opt-in and preserves complete backward compatibility. When the
exception-handlingfeature is disabled (the default), the compiler continues to emitabort()calls for throw statements exactly as before. Existing code compiles and runs identically. The new behavior only activates when explicitly enabled via--enable exception-handling, and even then the generated WASM modules only require runtime support that has been shipping unflagged in all major engines for over 4 years.Changes proposed in this pull request:
⯈ Implemented
throwstatement - Compiles to WebAssembly's nativethrowinstruction using a global$errortag that carries an i32 pointer to Error objects⯈ Implemented
try-catchblocks - Full support for catching exceptions with proper catch variable binding, flow analysis, and nested try-catch structures⯈ Implemented
try-finallyandtry-catch-finally- Complete finally support including the complex case ofreturnstatements inside try/catch blocks, using a pending action pattern to ensure finally always runs before control flow exitsImplementation Details
Exception Tag:
$errorcarrying an i32 pointer to Error objectensureExceptionTag()when first neededThrow Statement (
compileThrowStatement):Feature.ExceptionHandlingis enabled, generatesmodule.throw("$error", [valueExpr])abort()when feature is disabled (preserves existing behavior)FlowFlags.Throws | FlowFlags.Terminateson the flowTry-Catch (
compileTryStatement):try/catchblocks via Binaryenmodule.pop()to retrieve the exception value_BinaryenLocalSetto avoid shadow stack interference with pop placementTry-Finally with Return Support:
Uses a "pending action" pattern to defer returns until after finally executes:
pendingActionLocal(i32): tracks pending action (0=none, 1=return)pendingValueLocal: stores the pending return valueReturn in finally block overrides any pending return from try/catch:
Structure generated:
Core changes in
src/compiler.ts:exceptionTagEnsuredfield andensureExceptionTag()method for lazy tag creationcompileThrowStatement()updated to usemodule.throw()when feature enabledcompileTryStatement()completely rewritten with full try-catch-finally supportcompileReturnStatement()updated to check for try-finally contextSupporting changes in
src/flow.ts:tryFinallyPendingActionLocal- local index for pending action trackingtryFinallyPendingValueLocal- local index for pending return valuetryFinallyDispatchLabel- label to branch to for finally dispatchtryFinallyReturnType- return type for the pending valueisInTryFinallygetter andgetTryFinallyContext()methodTest Coverage
Basic Tests:
testThrow()- Basic throw statementtestTryCatch()- Basic try-catchtestCatchVar()- Accessing caught exception variable (e.message)testNoThrow()- Try-catch when no exception is throwntestFinally()- Basic finally blocktestNested()- Nested try-catch blocksFinally with Return Tests:
testReturnInCatchFinally()- Return in catch with finally (finally must run first)testTryCatchFinally()- Try-catch-finally without return in catchtestFinallyWithException()- Finally runs even when exception propagatestestFinallyNormalCompletion()- Finally with no exceptiontestReturnFromTry()- Return from try block with finallytestMultipleReturnsWithFinally()- Multiple return points with finallyClass-Based Tests:
CustomError- Custom error class extending ErrorResource- Resource management class with dispose patternCalculator- Class with try-catch in methods (divide,safeDivide)Outer/Inner- Nested class exception handlingStateMachine- State machine with exception-based error handlingCounter- Counter class with exception limitComplex Tests:
testArrayWithExceptions()- Array operations with exceptionstestRethrowWithFinally()- Rethrow with finally (verifies finally runs)testDeepNesting()- Deeply nested try-catch-finally tracking execution orderReturn in Finally Tests:
testReturnInFinally()- Return in finally overrides return in trytestReturnInFinallyOverridesCatch()- Return in finally overrides return in catchtestReturnInFinallySuppressesException()- Return in finally suppresses thrown exceptionLimitations
This implementation has one known limitation:
Usage
# Enable exception handling feature asc myfile.ts --enable exception-handling