Skip to content

Conversation

@BlobMaster41
Copy link

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-handling feature is disabled (the default), the compiler continues to emit abort() 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 throw statement - Compiles to WebAssembly's native throw instruction using a global $error tag that carries an i32 pointer to Error objects

Implemented try-catch blocks - Full support for catching exceptions with proper catch variable binding, flow analysis, and nested try-catch structures

Implemented try-finally and try-catch-finally - Complete finally support including the complex case of return statements inside try/catch blocks, using a pending action pattern to ensure finally always runs before control flow exits

Implementation Details

Exception Tag:

  • Single global exception tag $error carrying an i32 pointer to Error object
  • Matches JavaScript semantics where catch catches all exceptions
  • Tag is lazily created via ensureExceptionTag() when first needed

Throw Statement (compileThrowStatement):

  • When Feature.ExceptionHandling is enabled, generates module.throw("$error", [valueExpr])
  • Falls back to abort() when feature is disabled (preserves existing behavior)
  • Sets FlowFlags.Throws | FlowFlags.Terminates on the flow

Try-Catch (compileTryStatement):

  • Generates WebAssembly try/catch blocks via Binaryen
  • Catch variable is bound using module.pop() to retrieve the exception value
  • Uses direct _BinaryenLocalSet to avoid shadow stack interference with pop placement
  • Proper flow analysis merging try and catch paths

Try-Finally with Return Support:

  • Uses a "pending action" pattern to defer returns until after finally executes:

    1. pendingActionLocal (i32): tracks pending action (0=none, 1=return)
    2. pendingValueLocal: stores the pending return value
    3. Return statements branch to dispatch label instead of returning directly
    4. After finally code runs, dispatch logic performs the actual return
  • Return in finally block overrides any pending return from try/catch:

    • Flow tracking detects when finally contains a return statement
    • Dispatch logic is skipped when finally terminates
    • Properly suppresses exceptions (matches JavaScript semantics)
  • Structure generated:

    (block $finally_dispatch
      (try $try_finally
        (do ...)
        (catch_all
          ;; finally code
          (rethrow $try_finally)
        )
      )
    )
    ;; finally code (normal/return path)
    (if (i32.eq (local.get $pendingAction) (i32.const 1))
      (return (local.get $pendingValue))
    )

Core changes in src/compiler.ts:

  • exceptionTagEnsured field and ensureExceptionTag() method for lazy tag creation
  • compileThrowStatement() updated to use module.throw() when feature enabled
  • compileTryStatement() completely rewritten with full try-catch-finally support
  • compileReturnStatement() updated to check for try-finally context

Supporting changes in src/flow.ts:

  • tryFinallyPendingActionLocal - local index for pending action tracking
  • tryFinallyPendingValueLocal - local index for pending return value
  • tryFinallyDispatchLabel - label to branch to for finally dispatch
  • tryFinallyReturnType - return type for the pending value
  • isInTryFinally getter and getTryFinallyContext() method

Test Coverage

Basic Tests:

  • testThrow() - Basic throw statement
  • testTryCatch() - Basic try-catch
  • testCatchVar() - Accessing caught exception variable (e.message)
  • testNoThrow() - Try-catch when no exception is thrown
  • testFinally() - Basic finally block
  • testNested() - Nested try-catch blocks

Finally with Return Tests:

  • testReturnInCatchFinally() - Return in catch with finally (finally must run first)
  • testTryCatchFinally() - Try-catch-finally without return in catch
  • testFinallyWithException() - Finally runs even when exception propagates
  • testFinallyNormalCompletion() - Finally with no exception
  • testReturnFromTry() - Return from try block with finally
  • testMultipleReturnsWithFinally() - Multiple return points with finally

Class-Based Tests:

  • CustomError - Custom error class extending Error
  • Resource - Resource management class with dispose pattern
  • Calculator - Class with try-catch in methods (divide, safeDivide)
  • Outer/Inner - Nested class exception handling
  • StateMachine - State machine with exception-based error handling
  • Counter - Counter class with exception limit

Complex Tests:

  • testArrayWithExceptions() - Array operations with exceptions
  • testRethrowWithFinally() - Rethrow with finally (verifies finally runs)
  • testDeepNesting() - Deeply nested try-catch-finally tracking execution order

Return in Finally Tests:

  • testReturnInFinally() - Return in finally overrides return in try
  • testReturnInFinallyOverridesCatch() - Return in finally overrides return in catch
  • testReturnInFinallySuppressesException() - Return in finally suppresses thrown exception

Limitations

This implementation has one known limitation:

  • Break/continue in try-finally: Not yet implemented (would need action codes 2/3 and label management)

Usage

# Enable exception handling feature
asc myfile.ts --enable exception-handling
// Example: Resource cleanup pattern
class Resource {
  dispose(): void { /* cleanup */ }
}

function useResource(): i32 {
  let r = new Resource();
  try {
    // Do work that might throw
    return processData();
  } catch (e) {
    // Handle error
    return -1;
  } finally {
    // Always runs - cleanup resource
    r.dispose();
  }
}

// Example: Custom error
class ValidationError extends Error {
  constructor(message: string, public field: string) {
    super(message);
  }
}

function validate(value: i32): void {
  if (value < 0) {
    throw new ValidationError("Value must be positive", "value");
  }
}
  • I've read the contributing guidelines
  • I've added my name and email to the NOTICE file

Adds support for WebAssembly exception handling, including throw, try, catch, and finally constructs. The compiler now emits proper throw instructions and manages exception tags when the exception-handling feature is enabled. Try-finally and try-catch-finally blocks are compiled with correct control flow, including deferred returns and pending action dispatch. Flow context is extended to track try-finally state. Adds comprehensive tests for exception handling.
Added the 'enabled' flag for the exception-handling feature in features.json and updated the test logic in compiler.js to check this flag when determining missing features.
Simplified assertions in exception tests by removing redundant comparisons and using direct boolean checks. Updated type assertions in catch blocks for better type safety. Added eslint-disable comments for returns in finally blocks. Adjusted expected line numbers in .wat test outputs to match code changes.
@BlobMaster41 BlobMaster41 changed the title Experimental try catch finally feat: Experimental try catch finally Dec 12, 2025
@BlobMaster41 BlobMaster41 changed the title feat: Experimental try catch finally feat: Implement WebAssembly exception handling (try-catch-finally) Dec 12, 2025
@BlobMaster41 BlobMaster41 marked this pull request as draft December 12, 2025 03:28
Replaces array spread with manual array construction for block statements, improving clarity and potentially performance when combining tryBlock and dispatchStmts.
Updated the NOTICE file to include Anakun as a contributor.
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

Successfully merging this pull request may close these issues.

[Question] Try catch support

1 participant