Skip to content

JairusSW/try-as

Repository files navigation

╔╦╗╦═╗╦ ╦  ╔═╗╔═╗
 ║ ╠╦╝╚╦╝══╠═╣╚═╗
 ╩ ╩╚═ ╩   ╩ ╩╚═╝

npm version tests license

Table of Contents

Installation

npm install try-as

Add the transform to your asc build and load it last.

asc assembly/index.ts --transform try-as

Or in asconfig.json:

{
  "options": {
    "transform": ["try-as"]
  }
}

If you use multiple transforms, keep try-as last.

Usage

try-as rewrites try/catch/finally, throw, abort, unreachable, and selected stdlib throw paths so they can be handled through a consistent Exception object.

import { Exception } from "try-as";

try {
  throw new Error("boom");
} catch (e) {
  const err = e as Exception;
  console.log(err.toString()); // Error: boom
} finally {
  console.log("done");
}

How It Works

try-as is a source-to-source transform. It analyzes the AssemblyScript call graph, rewrites exception-producing code paths into helper state updates, and lowers each try/catch/finally into explicit control-flow checks.

AssemblyScript source
  -> source linker finds try blocks, throwing calls, methods, imports, and re-exports
  -> exception-aware functions/methods are marked
  -> throw/abort/unreachable are rewritten to helper state writes
  -> try/catch/finally becomes explicit do/break + catch-state checks
  -> catch receives a rebuilt Exception object
throw / abort / unreachable
  -> __ErrorState.error / __AbortState.abort / __UnreachableState.unreachable
  -> __ExceptionState.Failures++
  -> generated break/return exits the current rewritten scope
  -> nearest transformed catch checks __ExceptionState.shouldCatch(mask)
  -> new __Exception(__ExceptionState.Type) reconstructs the caught value

Conceptually, code like this:

try {
  mightFail();
} catch (e) {
  trace((e as Exception).toString());
}

is lowered into code shaped like this:

do {
  __try_mightFail();
  if (__ExceptionState.Failures > 0) break;
} while (false);

if (__ExceptionState.shouldCatch(/* throw|abort|unreachable */ <i32>14)) {
  let e = new __Exception(__ExceptionState.Type);
  __ExceptionState.Failures--;
  trace((e as Exception).toString());
}

The exact generated AST is more verbose, but that is the core model: transformed calls write shared exception state, generated control flow propagates it, and catch reconstructs a typed Exception.

Exception API

import { Exception, ExceptionType } from "try-as";
  • Exception.type: ExceptionType
  • Exception.toString(): string
  • Exception.is<T>(): bool
  • Exception.as<T>(): T
  • Exception.clone(): Exception
  • Exception.rethrow(): never

Exception.as<T>() supports Error subclasses, other managed objects, and primitive payloads like i32, bool, and f64. Exception.rethrow() uses the runtime method body. If err is statically typed as Exception, throw err; is rewritten as err.rethrow();. Other identifier throws still use the generated __try_rethrow() / rethrow() / raw throw fallback chain when available.

ExceptionType:

  • None
  • Abort
  • Throw
  • Unreachable

Examples

Catch abort and throw

import { Exception, ExceptionType } from "try-as";

try {
  abort("fatal");
} catch (e) {
  const err = e as Exception;
  if (err.type == ExceptionType.Abort) {
    console.log(err.toString()); // abort: fatal
  }
}

Type-safe custom errors

import { Exception } from "try-as";

class MyError extends Error {
  constructor(message: string) {
    super(message);
  }
}

try {
  throw new MyError("typed");
} catch (e) {
  const err = e as Exception;
  if (err.is<MyError>()) {
    const typed = err.as<MyError>();
    console.log(typed.message);
  }
}

Throwing non-Error values

throw is not limited to Error.

import { Exception } from "try-as";

class PlainThing {
  constructor(public label: string) {}

  toString(): string {
    return this.label;
  }
}

try {
  throw new PlainThing("plain");
} catch (e) {
  const err = e as Exception;
  if (err.is<PlainThing>()) {
    const value = err.as<PlainThing>();
    console.log(value.label); // plain
  }
}

Rethrow behavior

import { Exception } from "try-as";

try {
  // risky code
} catch (e) {
  const err = e as Exception;
  if (!err.is<Error>()) {
    throw err; // alias of err.rethrow() when `err` is typed as Exception
  }
}

Selective catch kinds

Use a // @try-as: ... comment immediately above a try to control which transformed exception kinds that catch should handle.

Accepted values are throw, abort, and unreachable, comma-separated in that exact format.

import { Exception } from "try-as";

try {
  // @try-as: throw,abort
  try {
    abort("selected");
  } catch (e) {
    console.log((e as Exception).toString()); // abort: selected
  }
} catch (_) {
  // only runs if the inner catch does not select that exception kind
}

Catching stdlib exceptions

Stdlib exceptions such as missing map keys, empty array pops, out-of-range string access, and malformed URI decode errors are catchable.

import { Exception } from "try-as";

try {
  new Map<string, string>().get("missing");
} catch (e) {
  const err = e as Exception;
  console.log(err.toString()); // Error: Key does not exist
}

Limitations

  • The selective catch directive must be written exactly as // @try-as: throw,abort,unreachable with the chosen kinds, immediately above the try.
  • Runtime/internal trap paths are intentionally not rewritten.
  • Exceptions from these internals are not catchable by try-as:
    • ~lib/rt
    • ~lib/shared
    • ~lib/wasi_
    • ~lib/performance
  • This library handles transformed throw/abort flows, not low-level Wasm traps like out-of-bounds memory faults.
  • throw err; becomes err.rethrow(); when err is statically typed as Exception.
  • Other identifier throws still use the generated __try_rethrow() / rethrow() fallback path when available.

Debugging

  • DEBUG=1 enables transform diagnostics.
  • WRITE=pathA,pathB writes transformed source snapshots as *.tmp.ts.

Example:

DEBUG=1 WRITE=./assembly/test.ts,~lib/map asc assembly/test.ts --transform try-as

Transform Modes

  • TRY_AS_REWRITE_STDLIB=0 disables stdlib throw rewriting.
  • TRY_AS_IMPORT_SCOPE=user injects helper imports only into user sources (all by default).
  • TRY_AS_DIAGNOSTICS=1 prints the active mode configuration at transform time.

Example:

TRY_AS_REWRITE_STDLIB=0 TRY_AS_IMPORT_SCOPE=user TRY_AS_DIAGNOSTICS=1 asc assembly/index.ts --transform try-as

Contributing

npm run build:transform
npm test
npm run format

License

This project is distributed under the MIT license.

Contact

About

Exception Handling for AssemblyScript

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

 

Packages

 
 
 

Contributors