close
Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Enforce Error type in throw statements
Throw statements now require the thrown value to be of type Error or a subclass. Added a diagnostic message for invalid throw types, updated the abort implementation to throw Error when exception handling is enabled, and added tests for invalid throw types.
  • Loading branch information
BlobMaster41 committed Jan 6, 2026
commit e751d276fd9361f044e5931d646a5e4998578d6f
13 changes: 13 additions & 0 deletions src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3024,6 +3024,19 @@ export class Compiler extends DiagnosticEmitter {
if (this.options.hasFeature(Feature.ExceptionHandling)) {
// Compile the thrown value - should be Error or subclass
let valueExpr = this.compileExpression(statement.value, Type.auto);
let valueType = this.currentType;

// Verify that the thrown type is Error or a subclass
let errorInstance = this.program.errorInstance;
let classReference = valueType.getClass();
if (!classReference || !classReference.isAssignableTo(errorInstance)) {
this.error(
DiagnosticCode.Only_Error_or_its_subclasses_can_be_thrown_but_found_type_0,
statement.value.range,
valueType.toString()
);
return module.unreachable();
}

// Ensure exception tag exists
let tagName = this.ensureExceptionTag();
Expand Down
1 change: 1 addition & 0 deletions src/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"Initializer, definitive assignment or nullable type expected.": 238,
"Definitive assignment has no effect on local variables.": 239,
"Ambiguous operator overload '{0}' (conflicting overloads '{1}' and '{2}').": 240,
"Only Error or its subclasses can be thrown, but found type '{0}'.": 241,

"Importing the table disables some indirect call optimizations.": 901,
"Exporting the table disables some indirect call optimizations.": 902,
Expand Down
8 changes: 8 additions & 0 deletions src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,14 @@ export class Program extends DiagnosticEmitter {
}
private _stringInstance: Class | null = null;

/** Gets the standard `Error` instance. */
get errorInstance(): Class {
let cached = this._errorInstance;
if (!cached) this._errorInstance = cached = this.requireClass(CommonNames.Error);
return cached;
}
private _errorInstance: Class | null = null;

/** Gets the standard `RegExp` instance. */
get regexpInstance(): Class {
let cached = this._regexpInstance;
Expand Down
21 changes: 20 additions & 1 deletion std/assembly/builtins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2603,13 +2603,32 @@ export abstract class i31 { // FIXME: usage of 'new' requires a class :(
// @ts-ignore: decorator
@external("env", "abort")
@external.js("throw Error(`${message} in ${fileName}:${lineNumber}:${columnNumber}`);")
declare function abort(
declare function __abort_impl(
message?: string | null,
fileName?: string | null,
lineNumber?: u32,
columnNumber?: u32
): void;

// When exception-handling is enabled, abort throws an Error that can be caught.
// When disabled, it calls the external abort function (host implementation).
function abort(
message: string | null = null,
fileName: string | null = null,
lineNumber: u32 = 0,
columnNumber: u32 = 0
): void {
if (isDefined(ASC_FEATURE_EXCEPTION_HANDLING)) {
let fullMessage = message ? message : "abort";
if (fileName) {
fullMessage += " in " + fileName + ":" + lineNumber.toString() + ":" + columnNumber.toString();
}
throw new Error(fullMessage);
} else {
__abort_impl(message, fileName, lineNumber, columnNumber);
}
}

// @ts-ignore: decorator
@external("env", "trace")
@external.js("console.log(message, ...[a0, a1, a2, a3, a4].slice(0, n));")
Expand Down
2,684 changes: 1,955 additions & 729 deletions tests/compiler/exceptions.debug.wat

Large diffs are not rendered by default.

3,055 changes: 1,942 additions & 1,113 deletions tests/compiler/exceptions.release.wat

Large diffs are not rendered by default.

77 changes: 77 additions & 0 deletions tests/compiler/exceptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,3 +499,80 @@ function testReturnInFinallySuppressesException(): i32 {
}
assert(testReturnInFinallySuppressesException() == 42);
assert(finallyReturnSuppressedExceptionRan);

// ============================================================
// Tests for catching abort() and runtime errors
// ============================================================

// Test catching abort()
function testCatchAbort(): bool {
let caught = false;
try {
abort("this should be catchable");
} catch (e) {
caught = true;
// Verify we got an Error with the abort message
assert(e.message.includes("this should be catchable"));
}
return caught;
}
assert(testCatchAbort());

// Test catching runtime errors from __new (allocation too large)
function testCatchRuntimeError(): bool {
let caught = false;
try {
// Try to allocate an impossibly large object
// This should trigger an allocation error in the runtime
__new(usize.MAX_VALUE, idof<ArrayBuffer>());
} catch (e) {
caught = true;
// Should have caught an allocation error
assert(e.message.length > 0);
}
return caught;
}
assert(testCatchRuntimeError());

// Test that abort in a function can be caught by the caller
function functionThatAborts(): void {
abort("abort from function");
}

function testCatchAbortFromFunction(): bool {
let caught = false;
try {
functionThatAborts();
} catch (e) {
caught = true;
assert(e.message.includes("abort from function"));
}
return caught;
}
assert(testCatchAbortFromFunction());

// Test catch variable is properly typed as Error
function testCatchVariableType(): bool {
try {
throw new Error("type test");
} catch (e) {
// e should be typed as Error, so we can access message directly
let msg: string = e.message;
return msg == "type test";
}
return false;
}
assert(testCatchVariableType());

// Test catching custom Error subclass (use existing CustomError class)
function testCatchCustomError2(): bool {
try {
throw new CustomError("custom error 2", 99);
} catch (e) {
// e is typed as Error, need to cast to access code
let custom = e as CustomError;
return custom.message == "custom error 2" && custom.code == 99;
}
return false;
}
assert(testCatchCustomError2());
9 changes: 9 additions & 0 deletions tests/compiler/throw-invalid-type.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"asc_flags": [
"--enable", "exception-handling"
],
"stderr": [
"AS241: Only Error or its subclasses can be thrown, but found type '~lib/string/String'.",
"EOF"
]
}
6 changes: 6 additions & 0 deletions tests/compiler/throw-invalid-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Test that throwing non-Error types results in a compile error

// This should fail - throwing a string is not allowed
throw "string error";

ERROR("EOF");
Loading