Skip to content

palashmon/learn-typed-errors

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Learn Typed Errors

A small learning project for typed error handling in TypeScript.

It compares three approaches:

  1. normal throw and try/catch
  2. plain TypeScript result unions
  3. neverthrow

It also shows one key benefit of typed errors:

When you add a new error type, TypeScript can show all places that now need to be updated.


Project goal

This project helps you understand four things:

  1. what is weak about normal exception-based error handling
  2. how plain TypeScript can make errors more explicit
  3. how neverthrow reduces some manual boilerplate
  4. how exhaustive type checks help when error types change

The project stays small on purpose.

There are only a few error types:

  • InvalidInput
  • NetworkError
  • ParseError

Then one more error is added later:

  • UnauthorizedError

That extra error is used to show how type checking catches missing handling.


What this project teaches

1. throw version

This shows the common style many projects use.

It uses:

  • throw
  • try/catch
  • generic runtime errors

Main issue:

  • error flow is hidden
  • function signatures do not show which errors can happen
  • adding new error cases does not force updates in all callers

2. TypeScript result union version

This shows a better baseline without any library.

It uses:

  • a custom Result<T, E>
  • tagged error objects
  • explicit success and failure values

Main benefit:

  • errors are visible in return types
  • callers must check success vs failure

Main cost:

  • more manual boilerplate

3. neverthrow version

This shows the same idea with a library built for Result handling.

It uses:

  • Result
  • ResultAsync
  • chaining methods like andThen, asyncAndThen, etc.

Main benefit:

  • cleaner composition
  • less manual branching in async flows

4. exhaustive type check demo

This is the most important part.

It shows what happens when a new error type is added.

Expected result:

  • type checking fails in places that do not handle the new error yet
  • once you add the missing case, type checking passes again

Folder guide

src/shared/

Shared building blocks used by all examples.

src/shared/types.ts

Common app types.

Example:

  • API data shape
  • UI/demo input modes

src/shared/errors.ts

All tagged error object types live here.

This is the main place to inspect the error model.

src/shared/assertNever.ts

Helper for exhaustive checks.

This is what makes missing switch cases fail during type checking.

src/shared/fakeApi.ts

Small fake async API.

It simulates:

  • success
  • network failure
  • bad JSON

src/throw-example/

Example using throw and try/catch.

src/throw-example/runThrowExample.ts

Shows the normal exception-based flow.

Use this folder to understand the current pain:

  • hidden error paths
  • runtime-only handling
  • weak guarantees

src/ts-union-example/

Example using plain TypeScript result unions.

src/ts-union-example/result.ts

Small custom Result<T, E> type.

src/ts-union-example/runTsUnionExample.ts

Same user flow as the throw version, but with explicit return types.

Use this folder to understand:

  • how far plain TypeScript already gets you
  • where manual boilerplate starts growing

src/neverthrow-example/

Example using neverthrow.

src/neverthrow-example/runNeverthrowExample.ts

Same user flow again, now with neverthrow.

Use this folder to compare:

  • less branching
  • cleaner async composition
  • same core idea as the TS union version

src/add-error-demo/

Files for the type-checking demo when a new error is added.

src/add-error-demo/initial.ts

Current working exhaustive switch.

This file handles the original error union.

src/add-error-demo/after-add-error.ts

Intentional failing example.

This file uses an extended error union but does not handle the new error yet.

Use this folder to see the key lesson:

  • add one new error
  • type check fails
  • fix the missing case
  • type check passes again

src/app/

Small UI helpers for the demo page.

src/app/render.ts

Renders the input, buttons, and output area.


src/main.ts

App entry file.

It wires the buttons to each example.


Requirements

Install these first:

  • Node.js 24
  • npm
  • Git

Use a recent Node.js version. If your Node version is too old, Vite may fail to start.


Setup steps

1. Clone the repo

git clone https://github.com/palashmon/learn-typed-errors.git cd learn-typed-errors

2. Install packages

npm install

3. Start the app

npm run dev

4. Open the local URL

Vite prints a local URL in the terminal.

It usually looks like this:

http://localhost:5173/ 

Open that in your browser.


Type check the project

Run this any time you want to check the TypeScript state:

npm run typecheck

Expected result at the start:

  • no errors

How to use the demo

Start the app:

npm run dev

Open the page in the browser.

You will see:

  • an input box
  • buttons for each example
  • an output area

Try the throw example

Click:

  • Throw: success
  • Throw: invalid input
  • Throw: network error

What to notice:

  • errors become plain runtime messages
  • the function signature does not clearly show all possible errors

Try the plain TypeScript union example

Click:

  • TS union: parse error

What to notice:

  • success and failure are explicit
  • error handling is more visible
  • there is more manual checking

Try the neverthrow example

Click:

  • neverthrow: success
  • neverthrow: invalid input
  • neverthrow: network error

What to notice:

  • same error model
  • cleaner flow than manual result handling
  • less repeated branching

Try the exhaustive switch demo

Click:

  • Exhaustive switch demo

What to notice:

  • shows message Bad response: JSON parse failed

Demo 1: add a new error and see type checking fail

This is the main learning step.

Step 1. Confirm the project passes type checking

Run:

npm run typecheck

Expected result:

  • no errors

Step 2. Open this file

src/add-error-demo/after-add-error.ts 

This file uses ExtendedAppError.

That union includes:

  • InvalidInput
  • NetworkError
  • ParseError
  • UnauthorizedError

But the switch in that file does not handle UnauthorizedError yet.

Step 3. Make the app use the failing formatter

In src/main.ts, switch usage from:

formatInitialError(...)

to:

formatExtendedError(...)

You can do this by importing formatExtendedError from:

src/add-error-demo/after-add-error.ts 

and using it instead of formatInitialError.

Step 4. Run type check again

npm run typecheck

Expected result:

  • TypeScript fails
  • the error points to the missing UnauthorizedError handling

This is the key benefit.

When the error union changes, the compiler helps you find missing updates.

Step 5. Fix the missing case

In src/add-error-demo/after-add-error.ts, add this case:

case "UnauthorizedError": return `Login required: ${error.message}`;

Step 6. Run type check again

npm run typecheck

Expected result:

  • no errors

This confirms the exhaustive handler is complete again.


Demo 2: remove an existing error and clean up dead code

This demo shows the opposite situation.

Instead of adding a new error, we remove an existing error from the union.

TypeScript will then show all places where that error is still handled but no longer possible.

This helps remove dead code safely.


Step 1. Confirm the project passes type checking

Run:

npm run typecheck

Expected result:

  • no errors

Step 2. Remove an error from the union

Open this file:

src/shared/errors.ts 

Find the AppError union.

Example before:

export type AppError = InvalidInputError | NetworkError | ParseError

Now remove one error. For this demo, remove ParseError.

Example after:

export type AppError = InvalidInputError | NetworkError

Save the file.


Step 3. Run type checking again

npm run typecheck

Expected result:

  • TypeScript now fails in several places.

The errors usually appear in switch statements or error handlers that still include:

case "ParseError":

Step 4. Remove the dead code

Go to each location TypeScript reports.

Delete the unused case.

Example before:

case "ParseError": return `Bad response: ${error.message}`;

Example after:

// removed because ParseError is no longer part of AppError

Repeat this for all reported locations.


Step 5. Run type checking again

npm run typecheck

Expected result:

  • no errors

This confirms that all code paths referencing the removed error are gone.


What this demo shows

Typed error unions help in two directions:

When you add a new error

  • TypeScript shows where handling is missing.

When you remove an error

  • TypeScript shows where dead code still exists.

This keeps error handling consistent as the system evolves.

About

Learn typed error handling in TypeScript

Resources

Stars

Watchers

Forks

Contributors