Skip to content

Latest commit

 

History

History
318 lines (245 loc) · 7.99 KB

File metadata and controls

318 lines (245 loc) · 7.99 KB

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

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