Skip to content

RFC: Checked Exceptions in TypeScript #2

Description

@JamesDHW

RFC: Checked Exceptions in TypeScript

A thread to discuss introducing checked exceptions to TypeScript.
Related GitHub Issue: read this first to see if your concern is expressed here.
Read first: About ErrorScript to see what problem it is trying to solve.

Status

There is a current implementation of this below, which is a prototype

Implementation Branch: ErrorScript: Checked Exceptions in TypeScript

Playground for Current Implementation
Current Specification

Critical Performances

1. Preserve JavaScript Semantics

TypeScript compiles to JavaScript and runs within its runtime model. Any checked error system must reflect the actual semantics of JavaScript rather than redefining them.

For synchronous code, JavaScript can throw exceptions.
For asynchronous code, JavaScript uses Promises, which can be rejected.

A proposal should:

  • Accurately model how failures occur at runtime.
  • Avoid introducing abstractions that distort or oversimplify JavaScript’s error model.
  • Avoid changing runtime behaviour or requiring new runtime constructs.

Static analysis should make existing behaviour visible rather than change it.


2. Gradual Adoption, Escape Hatches, and Ecosystem Compatibility

TypeScript is widely deployed across many codebases. Any proposal should support incremental adoption.

A viable system should:

  • Be opt-in and non-breaking for existing projects to continue using the latest TypeScript as-is.
  • Allow partial adoption within a codebase.
  • Interoperate with unannotated libraries and existing .d.ts files.
  • Provide reasonably required escape hatches consistent with existing TypeScript patterns.

New capabilities should strengthen type safety without degrading existing guarantees.


3. Performance

TypeScript’s performance is critical to developer experience.

A proposal should:

  • Impose negligible overhead when disabled.
  • Introduce only bounded and predictable overhead when enabled.
  • Avoid global analyses or complexity that scales poorly with large codebases.

Correctness improvements must not come at the cost of unacceptable compilation performance.


4. Language Coherence

Checked errors should integrate naturally into TypeScript’s existing type system.

A proposal should:

  • Reuse existing type-system concepts (e.g. structural typing, assignability, inference).
  • Follow the same mental models used for return types and parameters.
  • Avoid introducing parallel or orthogonal subsystems that behave differently from the rest of the language.

Error typing should feel like a natural extension of the type system, not a new programming paradigm layered on top of it.


5. Predictability and Local Reasoning

Developers should be able to understand error behaviour through local inspection of code and signatures.

A proposal should:

  • Prefer rules that are simple and explainable.
  • Avoid surprising interactions with control flow.
  • Minimise reliance on hidden global reasoning.

The system should reward clear, explicit code and make obligations at call sites obvious.


6. Minimal Surface Area

The initial design should focus on the smallest set of primitives required to enable static enforcement of error handling.

A proposal should:

  • Avoid bundling unrelated language features (e.g. opinionated syntactic sugar).
  • Leave room for future evolution without overcommitting the language prematurely.

Avoid carelessly adding new syntax which becomes embedded in every TypeScript codebase with unintended consequences for code quality.


7. Tooling Alignment

Any checked error system should:

  • Produce clear, actionable diagnostics.
  • Integrate with existing suppression and configuration mechanisms.
  • Support ergonomic code actions consistent with current TypeScript workflows.

The experience should guide developers toward correct handling without introducing excessive friction.


8. Long-Term Stability of the TypeScript Codebase

A proposal should:

  • Be maintainable within the compiler architecture.
  • Avoid fragile semantics that are difficult to evolve.
  • Prioritise durability and clarity over maximal expressiveness in the first iteration.

Features & Decisions

The major design dimensions of checkedErrors, the available options considered for each, an evaluation against the stated Critical Performance, and the selected decision.


1. Failure Model: One Channel vs Two Channels

Options

  1. Single unified failure channel (merge throws and rejects into one effect type).
  2. Two distinct channels:
    • throws (synchronous, call-time)
    • rejects (Promise settlement-time)

Evaluation

Criteria Single Channel Two Channels
Preserve JS Semantics ⚠️ Simplifies runtime model ✅ Mirrors JS exactly
Language Coherence ⚠️ Requires abstraction over JS ✅ Matches existing JS constructs
Predictability ⚠️ Async edge cases blurred ✅ Clear sync/async separation
Minimal Surface Area ✅ Simpler conceptually ⚠️ Two keywords
Ecosystem Compatibility ⚠️ Diverges from JS mental model ✅ Aligns with async/await

Decision: Two distinct channels (throws and rejects).

Why: JavaScript has two failure mechanisms with different timing semantics. Modeling both explicitly preserves runtime fidelity and avoids unsound simplifications.


2. Enforcement Location: Call-Site vs Declaration-Site Only

Options

  1. Declaration-only enforcement (require effect clauses but do not enforce at every call).
  2. Call-site enforcement (require each potentially failing call to be handled or propagated).

Evaluation

Criteria Declaration-Only Call-Site Enforcement
Predictability ⚠️ Hidden obligations ✅ Explicit obligations
Language Coherence ⚠️ Diverges from TS return typing ✅ Mirrors return-type checking
Gradual Adoption ⚠️ Weak safety guarantees ✅ Strong static guarantees
Minimal Surface Area ✅ Smaller rule set ⚠️ More diagnostics

Decision: Call-site enforcement.

Why: Obligations are most visible and understandable at the usage boundary, consistent with how TypeScript enforces return types and parameter assignability.


3. Handling Semantics: Syntactic vs Control-Flow Dominance

Options

  1. Control-flow dominance analysis (treat a throw as handled if guaranteed by CFG reasoning).
  2. Syntactic containment rule (handled iff inside a try with catch).

Evaluation

Criteria CFG/Dominance Syntactic Rule
Predictability ⚠️ Complex reasoning ✅ Simple and local
Performance ⚠️ Expensive analysis ✅ Bounded cost
Language Coherence ⚠️ New reasoning model ✅ Aligns with existing TS patterns
Minimal Surface Area ❌ Adds complexity ✅ Minimal rule

Decision: Syntactic containment rule.

Why: The syntactic rule is explainable, locally checkable, and consistent with TypeScript’s existing checker architecture.


4. Effect Propagation Model

Options

  1. Require explicit effect annotations everywhere.
  2. Infer effects and allow implicit propagation.

Evaluation

Criteria Explicit Everywhere Inference + Propagation
Gradual Adoption ❌ High friction ✅ Low friction
Language Coherence ⚠️ New burden on users ✅ Matches TS inference philosophy
Predictability ✅ Explicit ✅ Inferred but visible in signatures
Ecosystem Compatibility ❌ Large migration cost ✅ Incremental

Decision: Inferred effects with implicit propagation.

Why: TypeScript already relies heavily on inference. Effects behave analogously to return types and propagate automatically unless locally handled.


5. Declared Effects vs Inferred Effects

Options

  1. Declared effects are advisory only.
  2. Declared effects are authoritative contracts (error where declared is unasignalble to inferred).

Evaluation

Criteria Advisory Contractual
Language Coherence ❌ Weak contract model ✅ Mirrors return types
Predictability ❌ Inconsistent ✅ Strong guarantees
Gradual Adoption ⚠️ Easier migration ⚠️ Stricter validation
Long-Term Stability ❌ Fragile ✅ Durable

Decision: Declared effects are authoritative contracts.

Why: Inferred effects must be assignable to declared throws/rejects clauses, just as return types must be assignable to declared return types. This preserves consistency with TypeScript’s type contract model.


6. Exhaustiveness in catch

Options

  1. Require exhaustive handling of union error types.
  2. Rely on standard TypeScript narrowing without enforcing exhaustiveness.

Evaluation

Criteria Enforced Exhaustiveness Narrowing Only
Minimal Surface Area ❌ Adds new rules ✅ Reuses existing narrowing
Predictability ⚠️ Stricter than JS norms ✅ Familiar TS behavior
Language Coherence ⚠️ Introduces special-case semantics ✅ Uses existing control-flow typing
Performance ⚠️ More analysis ✅ No additional cost

Decision: No automatic exhaustiveness requirement. Any unhandled error gets propagated as the narrowed type.

Why: Catch variable typing integrates with existing TypeScript narrowing. Exhaustiveness can be achieved using standard patterns (assertNever), without introducing new enforcement rules.


7. Recursion and Cycles

Options

  1. Full fixpoint computation of recursive effects.
  2. Degrade strongly connected components to unknown.

Evaluation

Criteria Full Fixpoint Degrade to unknown
Performance ❌ Potentially expensive ✅ Bounded
Predictability ⚠️ Hard to reason about ✅ Conservative and clear
Long-Term Stability ⚠️ Complex ✅ Simpler implementation

Decision: Degrade cyclic inference to unknown.

Why: Ensures bounded complexity and predictable behavior without introducing global fixpoint analysis. Simplicity and can be introduced later if needed without breaking existing code.


8. Escape Hatches

Options

  1. No escape hatches beyond any/casts.
  2. Provide targeted suppression directive (@ts-expect-exception).

Evaluation

Criteria No Targeted Hatch Targeted Directive
Gradual Adoption ❌ Hard migration ✅ Incremental
Language Coherence ⚠️ Forces unsafe casts ✅ Mirrors existing directives
Predictability ⚠️ Encourages any ✅ Explicit suppression
Tooling Alignment ⚠️ Inconsistent ✅ Consistent with TS workflow

Decision: Provide // @ts-expect-exception for checkedErrors diagnostics.

Why: Supports incremental adoption without encouraging unsafe type erasure. Existing TS escape mechanisms remain available, new directive is required to allow incrementally ignoring error checking without the need to use @ts-ignore which also ignores type errors.


9. Fire-and-Forget Semantics

Options

  1. Allow dropped promises silently.
  2. Require explicit acknowledgment (void or .catch).

Evaluation

Criteria Silent Drop Explicit Acknowledgment
Preserve JS Semantics ✅ JS allows it ✅ JS allows it
Predictability ❌ Hidden failure ✅ Explicit intent
Language Coherence ⚠️ Weakens static guarantee ✅ Aligns with checked model
Gradual Adoption ⚠️ Less safe ⚠️ Slight friction

Decision: Require explicit acknowledgment for dropped rejections.

Why: Silent failure undermines the purpose of checked errors. void serves as an explicit statement of intent without altering runtime semantics or adding a new concept.


10. Error Matching Syntax

Options

  1. No new syntax (use existing TypeScript narrowing)

    • Match errors using instanceof, typeof, equality checks, user-defined type guards, and assertion functions.
  2. Multi-catch / typed binding syntax

    • Examples (illustrative): catch (NetworkError as e), catch (NetworkError | StorageError as e)
  3. Pattern matching / conditional catch

    • Examples (illustrative): catch (e) when isNetworkError(e), catch case NetworkError(e) => ...
  4. Exhaustive catch constructs

    • New syntax that implies or enforces exhaustiveness and default rethrow behavior.

Evaluation (against Evaluation Criteria)

Option Preserve JS Semantics Gradual Adoption & Ecosystem Performance Language Coherence Predictability & Local Reasoning Minimal Surface Area Tooling Alignment Long-Term Stability
(1) No new syntax
(2) Multi-catch / typed binding ⚠️ (new syntax to learn) ⚠️ (new grammar + rules) ⚠️ (new typing edge cases) ⚠️
(3) Pattern matching / conditional catch ⚠️ (semantics questions) ⚠️ ⚠️ (new analysis) ⚠️ ⚠️ ⚠️ ⚠️
(4) Exhaustive catch constructs ⚠️ ⚠️ (behavioral shift) ⚠️ ⚠️ ⚠️ (new obligations) ⚠️ ⚠️

Decision: Do not introduce new error-matching syntax in the initial checkedErrors proposal.

Why:

  • The core value of checkedErrors (typed failures + enforcement) is achievable using existing JavaScript try/catch and existing TypeScript narrowing.
  • Adding new matching syntax significantly increases language surface area and pulls in secondary debates (multi-catch shape, conditional guards, exhaustiveness expectations, cross-realm concerns, structural matching semantics).
  • Keeping v1 focused improves predictability, minimizes implementation risk, and supports incremental ecosystem adoption.
  • Syntax improvements can be explored later as separate, optional proposals once the underlying effect model is validated.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions