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
- Single unified failure channel (merge throws and rejects into one effect type).
- 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
- Declaration-only enforcement (require effect clauses but do not enforce at every call).
- 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
- Control-flow dominance analysis (treat a throw as handled if guaranteed by CFG reasoning).
- 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
- Require explicit effect annotations everywhere.
- 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
- Declared effects are advisory only.
- 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
- Require exhaustive handling of union error types.
- 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
- Full fixpoint computation of recursive effects.
- 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
- No escape hatches beyond
any/casts.
- 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
- Allow dropped promises silently.
- 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
-
No new syntax (use existing TypeScript narrowing)
- Match errors using
instanceof, typeof, equality checks, user-defined type guards, and assertion functions.
-
Multi-catch / typed binding syntax
- Examples (illustrative):
catch (NetworkError as e), catch (NetworkError | StorageError as e)
-
Pattern matching / conditional catch
- Examples (illustrative):
catch (e) when isNetworkError(e), catch case NetworkError(e) => ...
-
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.
RFC: Checked Exceptions in TypeScript
Status
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
throwexceptions.For asynchronous code, JavaScript uses
Promises, which can be rejected.A proposal should:
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:
.d.tsfiles.New capabilities should strengthen type safety without degrading existing guarantees.
3. Performance
TypeScript’s performance is critical to developer experience.
A proposal should:
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:
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:
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 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:
The experience should guide developers toward correct handling without introducing excessive friction.
8. Long-Term Stability of the TypeScript Codebase
A proposal should:
Features & Decisions
1. Failure Model: One Channel vs Two Channels
Options
throws(synchronous, call-time)rejects(Promise settlement-time)Evaluation
2. Enforcement Location: Call-Site vs Declaration-Site Only
Options
Evaluation
3. Handling Semantics: Syntactic vs Control-Flow Dominance
Options
trywithcatch).Evaluation
4. Effect Propagation Model
Options
Evaluation
5. Declared Effects vs Inferred Effects
Options
Evaluation
6. Exhaustiveness in
catchOptions
Evaluation
7. Recursion and Cycles
Options
unknown.Evaluation
unknown8. Escape Hatches
Options
any/casts.@ts-expect-exception).Evaluation
any9. Fire-and-Forget Semantics
Options
voidor.catch).Evaluation
10. Error Matching Syntax
Options
No new syntax (use existing TypeScript narrowing)
instanceof,typeof, equality checks, user-defined type guards, and assertion functions.Multi-catch / typed binding syntax
catch (NetworkError as e),catch (NetworkError | StorageError as e)Pattern matching / conditional catch
catch (e) when isNetworkError(e),catch case NetworkError(e) => ...Exhaustive catch constructs
Evaluation (against Evaluation Criteria)