Skip to content

ErrorScript: Checked Exceptions in TypeScript#1

Open
JamesDHW wants to merge 20 commits intomainfrom
errorscript
Open

ErrorScript: Checked Exceptions in TypeScript#1
JamesDHW wants to merge 20 commits intomainfrom
errorscript

Conversation

@JamesDHW
Copy link
Owner

@JamesDHW JamesDHW commented Feb 15, 2026

Inferred Checked Errors — Proof of Concept Specification

Related GitHub Issue: microsoft#13219

1. Purpose

This feature introduces checked error semantics to TypeScript. Effects may be inferred from bodies OR declared at declaration sites via throws / rejects. Inference remains the default; declarations exist primarily for .d.ts boundaries and (optionally) as contracts for .ts.

The goal is to experimentally evaluate:

  • Whether inferred checked errors are ergonomically viable in TypeScript.
  • Whether error handling can be enforced statically with inference and optional declarations.
  • How well such a system integrates with async/Promise-based JavaScript.

This is a compiler-level feature and cannot be implemented as an ESLint rule or language service plugin.

2. High-Level Behavior

The system enforces that:

  • Any expression that may throw (or reject) must either be handled locally via try/catch or explicitly propagated.

Thrown/reject effects come from inference (see Core Semantics) or from declaration-site throws / rejects (see Declaration-Site Effects). When inferring, sources include:

  • throw statements
  • Calls to other functions
  • Declaration-site effects on callees (e.g. in .d.ts or declared on .ts signatures)

3. Core Semantics

3.1 Thrown Type Inference

Rule 1 — throw expr

throw expr;

Contributes the type: TypeOf(expr).

Examples:

throw new Error()         Error
throw new FooError()      FooError
throw ""                  string
throw 123                 number

There is no restriction on thrown types.

Rule 2 — throw; (rethrow)

Inside a catch (e) block:

throw;

Contributes: TypeOf(e).

3.2 Function Thrown Type

For a function f, its inferred thrown type is:

Union(
  direct throws in body,
  thrown types of any called functions,
  rethrows
)

If a cycle is detected in call graph analysis:

  • ThrownType = unknown

This is an intentional PoC simplification.

3.3 Call Site Enforcement

If a call expression may throw type E, then it must satisfy one of:

  • Be enclosed within a try that has a catch
  • Be inside another function that is itself inferred to throw (i.e. propagation)
  • For async calls: be awaited inside a try, or explicitly handled via .catch(...)
  • Be explicitly ignored using void (see async section)

Otherwise, a compile-time error is produced:

  • Unhandled thrown type: E

3.4 Declaration-Site Effects (throws / rejects)

Function, method, and constructor signatures may include:

  • throws E — for synchronous exceptions
  • rejects E — for Promise rejections

These clauses are allowed in .d.ts and in .ts.

Precedence:

  • If a function has a body and no declared clause → infer effects from the body (existing behavior).
  • If a function has a body and a declared clause is present → the declared clause is the contract; the body must be sound relative to it (see validation).
  • If a signature has no body (e.g. .d.ts) → the declared clause is the only source of truth (no inference).

4. try/catch/finally Semantics

4.1 Absorption Rule

For:

try {
  TRY
} catch (e) {
  CATCH
} finally {
  FINALLY
}

Define:

  • T_try = thrown type of TRY
  • T_catch = thrown type of CATCH
  • T_finally = thrown type of FINALLY

Then:

  • If catch exists: Thrown(tryStatement) = T_catch | T_finallyT_try is considered handled (absorbed).
  • If no catch exists: Thrown(tryStatement) = T_try | T_finally

4.2 Catch Variable Typing

For:

try {
  ...
} catch (e) {
  ...
}

The type of e is: union of all types thrown from TRY.

Example:

try {
  if (x) throw new FooError()
  else throw new BarError()
} catch (e) {
  // e: FooError | BarError
}

This type supports standard TypeScript narrowing (e.g. instanceof).

5. Async / Promise Semantics

5.1 Async Functions

For:

async function f() { ... }

If the function body infers thrown type E, then f(): Promise<T> is considered a promise that may reject with E. This is treated as an effect attached to the promise value.

5.2 Enforcement on await

For:

await expr

If expr is a promise that may reject with type E, then:

  • The await must be inside a try/catch, or
  • The enclosing function must propagate the error.

Otherwise compile error: unhandled rejection type E.

5.3 Fire-and-Forget (Stance 2)

The following are allowed patterns:

Explicit ignore

void f();

This suppresses enforcement.

Explicit catch

f().catch(() => {});

Any .catch(...) counts as handling.

Disallowed

f(); // error if f returns Promise that may reject

Unless explicitly ignored or handled.

5.4 Promise.all

If:

  • p1 rejects E1
  • p2 rejects E2

Then Promise.all([p1, p2]) produces Promise<...> rejecting with (E1 | E2).

Thus:

await Promise.all([a(), b()])

Requires handling of E1 | E2.

6. Standard Library and Declaration Files

.d.ts functions can declare throws / rejects at the declaration site; that is the primary mechanism for effects without a body. Curated stdlib mapping is optional as a bootstrap/migration tool, not the core plan—e.g. a small map may be used to seed declarations or for migration, but the main path is declaration-site effects in .d.ts. For declarations with no body and no declared clause, behavior is implementation-defined (e.g. never or conservative unknown; must be consistent).

7. Recursion Handling

If call graph analysis detects recursion:

  • ThrownType = unknown

This avoids fixpoint complexity in the PoC. Future improvements may replace this with a recursive type marker.

8. Non-Goals (PoC Scope)

The following are explicitly out of scope:

  • Mandatory effect annotations on every function
  • Perfect interprocedural analysis
  • Precise modeling of all Promise combinators
  • Exhaustive stdlib throw coverage
  • Soundness guarantees across all JS patterns
  • Runtime enforcement

9. Compile-Time Errors Introduced

New error class:

  • Unhandled thrown type: E

And for async:

  • Unhandled promise rejection type: E

These errors occur when:

  • A throwing call is not in try/catch
  • An awaited promise rejection is not handled
  • A rejecting promise is dropped without .catch or void

10. Expected Developer Experience

Before

foo(); // may throw

After

try {
  foo();
} catch (e) {
  ...
}

Or propagate:

function bar() {
  foo(); // allowed because bar now inferred to throw
}

Async

try {
  await foo();
} catch (e) {
  ...
}

Or explicitly ignore:

void foo();

11. Design Principles

  • Effects may be inferred from bodies OR declared at declaration sites via throws / rejects; inference remains the default; declarations exist primarily for .d.ts boundaries and (optionally) as contracts for .ts.
  • JS-compatible (anything can be thrown).
  • Try/catch absorbs.
  • Async integrates via rejection.
  • Explicit ignore required for fire-and-forget.
  • Cycles degrade to unknown.

12. Intended Outcome of the PoC

This feature should allow evaluation of:

  • Whether inference plus optional declarations are usable in a JS ecosystem.
  • Whether Promise rejection typing improves correctness.
  • Whether explicit ignore (void) is sufficient ergonomically.
  • Whether declaration-site effects are necessary for adoption.
  • Whether inference noise becomes unmanageable.

@JamesDHW JamesDHW changed the title Errorscript Errorscript: checked exceptions in TypeScript Feb 15, 2026
@JamesDHW JamesDHW changed the title Errorscript: checked exceptions in TypeScript ErrorScript: checked exceptions in TypeScript Feb 15, 2026
@JamesDHW JamesDHW changed the title ErrorScript: checked exceptions in TypeScript ErrorScript: Checked Exceptions in TypeScript Feb 15, 2026
Comment on lines +1821 to +1822
throwsType?: TypeNode,
rejectsType?: TypeNode,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps, for better type-safety

Suggested change
throwsType?: TypeNode,
rejectsType?: TypeNode,
throwsType: TypeNode | undefined,
rejectsType: TypeNode | undefined,

Comment on lines +1851 to +1852
throwsType?: TypeNode,
rejectsType?: TypeNode,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throwsType?: TypeNode,
rejectsType?: TypeNode,
throwsType: TypeNode | undefined,
rejectsType: TypeNode | undefined,

Comment on lines +1876 to +1877
throwsType?: TypeNode,
rejectsType?: TypeNode,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throwsType?: TypeNode,
rejectsType?: TypeNode,
throwsType: TypeNode | undefined,
rejectsType: TypeNode | undefined,

Comment on lines +1937 to +1938
throwsType?: TypeNode,
rejectsType?: TypeNode,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throwsType?: TypeNode,
rejectsType?: TypeNode,
throwsType: TypeNode | undefined,
rejectsType: TypeNode | undefined,

Comment on lines +2058 to +2059
throwsType?: TypeNode,
rejectsType?: TypeNode,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throwsType?: TypeNode,
rejectsType?: TypeNode,
throwsType: TypeNode | undefined,
rejectsType: TypeNode | undefined,

Comment on lines +2189 to +2190
throwsType?: TypeNode,
rejectsType?: TypeNode,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throwsType?: TypeNode,
rejectsType?: TypeNode,
throwsType: TypeNode | undefined,
rejectsType: TypeNode | undefined,

Comment on lines +2213 to +2214
throwsType?: TypeNode,
rejectsType?: TypeNode,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throwsType?: TypeNode,
rejectsType?: TypeNode,
throwsType: TypeNode | undefined,
rejectsType: TypeNode | undefined,

Comment on lines +2358 to +2359
throwsType?: TypeNode,
rejectsType?: TypeNode,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throwsType?: TypeNode,
rejectsType?: TypeNode,
throwsType?: TypeNode | undefined,
rejectsType?: TypeNode | undefined,

Comment on lines +2383 to +2384
throwsType?: TypeNode,
rejectsType?: TypeNode,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throwsType?: TypeNode,
rejectsType?: TypeNode,
throwsType: TypeNode | undefined,
rejectsType: TypeNode | undefined,

Comment on lines +3226 to +3227
throwsType?: TypeNode,
rejectsType?: TypeNode,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throwsType?: TypeNode,
rejectsType?: TypeNode,
throwsType: TypeNode | undefined,
rejectsType: TypeNode | undefined,

Comment on lines +3278 to +3279
throwsType?: TypeNode,
rejectsType?: TypeNode,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throwsType?: TypeNode,
rejectsType?: TypeNode,
throwsType: TypeNode | undefined,
rejectsType: TypeNode | undefined,

Comment on lines +3302 to +3303
throwsType?: TypeNode,
rejectsType?: TypeNode,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throwsType?: TypeNode,
rejectsType?: TypeNode,
throwsType: TypeNode | undefined,
rejectsType: TypeNode | undefined,

Comment on lines +3346 to +3347
throwsType?: TypeNode,
rejectsType?: TypeNode,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throwsType?: TypeNode,
rejectsType?: TypeNode,
throwsType: TypeNode | undefined,
rejectsType: TypeNode | undefined,

Comment on lines +4365 to +4366
throwsType?: TypeNode,
rejectsType?: TypeNode,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throwsType?: TypeNode,
rejectsType?: TypeNode,
throwsType: TypeNode | undefined,
rejectsType: TypeNode | undefined,

Comment on lines +4421 to +4422
throwsType?: TypeNode,
rejectsType?: TypeNode,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throwsType?: TypeNode,
rejectsType?: TypeNode,
throwsType: TypeNode | undefined,
rejectsType: TypeNode | undefined,

return false;
}

function parseEffectClause(): { throwsType?: TypeNode; rejectsType?: TypeNode } {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function parseEffectClause(): { throwsType?: TypeNode; rejectsType?: TypeNode } {
function parseEffectClause(): { throwsType: TypeNode | undefined; rejectsType: TypeNode | undefined } {

} from "./_namespaces/ts.js";
import * as performance from "./_namespaces/ts.performance.js";

const THROWS_ERROR_CODES = new Set([18063, 18064]);
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps find a better place to put this to separate it from the business logic

* Test for whether a single line comment with leading whitespace trimmed's text contains a directive.
*/
const commentDirectiveRegExSingleLine = /^\/\/\/?\s*@(ts-expect-error|ts-ignore)/;
const commentDirectiveRegExSingleLine = /^\/\/\/?\s*@(ts-expect-error|ts-expect-exception|ts-ignore)/;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: this new directive name must not regex match with an existing one (e.g. ts-ignore-exception also matches with ts-ignore, which ignores the next line altogether (unwanted - we only want to disable exception checking)

return fileEmitMode === ModuleKind.CommonJS ? ModuleKind.CommonJS :
emitModuleKindIsNonNodeESM(fileEmitMode) || fileEmitMode === ModuleKind.Preserve ? ModuleKind.ESNext :
undefined;
undefined;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: through the rest of the PR, will need to recheck formatting and lint to correct some of these issues and reduce the diff

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.

1 participant