A fully type-safe and lightweight way of using exceptions instead of throwing errors

Overview

๐Ÿ›ก๏ธ exceptionally

A fully type-safe and lightweight way of using exceptions instead of throwing errors

๐Ÿฆบ fully typesafe
๐Ÿค lightweight (209 bytes)
๐Ÿƒ almost no runtime overhead
๐Ÿ‘Œ easy to use syntax
๐Ÿค works everywhere (browser, node, cjs, esm)
โš ๏ธ can warn you about unhandled exceptions
โ›” no external dependencies

Table of contents

Problem Description

Code can fail. Especially when you are accessing multiple services. The common way to handle errors is to throw them. But you won't really know what function could potentially throw an error in your application code.

Well written applications can differentiate between errors and exceptions. They can recover from exceptions and include ways to recover from them.

Wrapping everything into try-catch blocks is not a good approach since it requires you to know the implementation of the function you are calling adds a indentation level, alters the program flow and is easy to forget if you are not paying attention.

The exceptions you get in the catch block typed as unknown, so you don't really know what happened and you need to account for different kind of exceptions (e.g. retry sending a request makes only sense if you got a network exception and will probably not make sense if you pass invalid payload to a service).

While it requires just a little of effort to look into a function to see what kind of exception get thrown, you probably can handle it manually. But in bigger applications you will probably have a lot of nesting and conditional logic where it is not so easy to spot all different outcomes. It is easy to forget to handle an exception case or maybe you want to handle a case that was already handled inside that function, so you'll end up with code that will never be reached and

Adding a new kind of exception deep down in the nested functions would require you to take a look at all the code parts that use the function and check whether they should handle the exception or pass it to the next level.

Solution

Don't throw errors and exceptions, return them instead. That's it!

No, there is a little bit more to it.

First of all, we need to make sure that in each part of the code we kow what the outcome of a specific function call will be, without looking at the implementation. To do that, we always need to return data as well as exceptions. If we return everything, TypeScript can infer all types and we know what we get when calling a function.

But now we also need a way to distinguish between a successful result and a result that contains an exception, so we need to wrap the value we return into an object. A by-product of this is that we need to unwrap the actual value at a later point, where we want to access it. This should be made as easiest as possible.

Because we don't throw exceptions, we don't use try-catch blocks. We need to use if statements to check whether or result contains a successful response or an exception.

Little helper functions, that are fully typed will greatly improve the Developer Experience. Of course we want our code to be explicit enough, so the code can be read and understood fast. This means we need to come up with meaningful names for our wrapping functions.

And because this is no rocket science, we don't need hundreds of dependencies to make this all happen. The could should be kept clean and efficient.

This packages delivers a solution to all the problems described above.

Installation

npm install exceptionally

Usage

You can find some detailed examples here.

exposed functions

  • success:

    const success: <Data>(data: Data) => Success<Data>
    import { success } from 'exceptionally'
    
    const saySomething = () => {
    	return success('hello world')
    }
    
    const result = saySomething()
    
    result.isSuccess // => `true`
    result.isException // => `false`
    result() // => `'hello world'`
  • exception:

    const exception: <Data>(data: Data) => Exception<Data>
    import { exception } from 'exceptionally'
    
    const saySomething = () => {
    	return exception("Don't tell me what to do!")
    }
    
    const result = saySomething()
    
    result.isSuccess // => `false`
    result.isException // => `true`
    result() // => `"Don't tell me what to do!"`
  • assertSuccess:

    const assertSuccess: <Result extends Success<unknown>>(result: Result) => asserts result is Result
    import { assertSuccess, exception } from 'exceptionally'
    
    const doSomething = () => {
    	const result = Math.random() > 0.5 ? success(1) : exception(0)
    
    	if (result.isException) throw new Error(result())
    
    	assertSuccess(result)
    
    	return success()
    }
  • assertException:

    const assertException: <Result extends Exception<unknown>>(result: Result) => asserts result is Result
    import { assertException, exception } from 'exceptionally'
    
    const doSomething = () => {
    	const result = Math.random() > 0.5 ? success(1) : exception(0)
    
    	if (result.isSuccess) return result()
    
    	assertException(result)
    	throw new Error(result())
    }
  • Exceptionally:

    type Inverted<Success extends boolean> = Success extends true ? false : true
    
    class Exceptionally<Success extends boolean> {
    	readonly isSuccess: Success
    	readonly isException: Inverted<Success>
    }
    import { success, Exceptionally } from 'exceptionally'
    
    const result = Math.random() > 0.5 ? success(1) : 0
    
    if (result instanceOf Exceptionally) {
       const data = result()
       console.info(data) // => `1`
    } else {
       console.info(result) // => `0`
    }

exposed types

  • ExceptionallyResult:

    type ExceptionallyResult<Success extends boolean, Data> = () => Data & Exceptionally<Success>
  • Success

    type Success<Data> = ExceptionallyResult<true, Data>
  • Exception

    type Exception<Data> = ExceptionallyResult<false, Data>
  • ExtractDataType

    type ExtractDataType<Result extends ExceptionallyResult<boolean, unknown>> = Result extends
    	ExceptionallyResult<boolean, infer Data> ? Data : never
    import { ExtractDataType, success } from 'exceptionally'
    
    const result = success(1)
    
    type Data = ExtractDataType<typeof result> // => `number`
  • ExtractSuccessType

    type ExtractSuccessType<Result extends ExceptionallyResult<boolean, unknown>> = Result extends
    	ExceptionallyResult<true, infer Data> ? Success<Data> : never
    import { exception, ExtractSuccessType, success } from 'exceptionally'
    
    const result = Math.random() > 0.5 ? success(new Date()) : exception('error')
    
    type Data = ExtractSuccessType<typeof result> // => `Success<Date>`
  • ExtractExceptionType

    type ExtractExceptionType<Result extends ExceptionallyResult<boolean, unknown>> = Result extends
    	ExceptionallyResult<false, infer Data> ? Exception<Data> : never
    import { exception, ExtractExceptionType, success } from 'exceptionally'
    
    const result = Math.random() > 0.5 ? success(new Date()) : exception('error')
    
    type Data = ExtractExceptionType<typeof result> // => `Exception<string>`

Best Practices

  • create wrapper functions for calls to other services
    Keep it DRY. Once you have written the code to connect to a service, you can reuse it for different API calls. And you don't need to handle the same edge-cases multiple times.
  • internally don't throw anything, just throw errors at the application boundaries
    Inside the code you can control, never throw errors. But you need to tell your users and services that consume data from your application if something was not successful. At that point it is ok to throw an Error.
  • document what kind of errors your application could throw and use a unique class (or error code) per error
    Having an unique meaningful identifier for each kind of error (e.g. validation, network-issues, etc.) will help you understand what has happened even after 3 or more levels of function calls. It makes it easy to handle only specific exceptions and deliver better error messages to your users.

Glossary

error

It is not possible to recover from an error.

e.g. a OutOfMemoryError will hinder your application to execute it's code and therefore you can probably do little to nothing against it. The result will probably lead to an exit of the application.

exception

Exceptions are caused by the code of the application itself. The application knows this case could occur and can recover from it.

e.g. a ValidationException will not store the data in your database, but will also not crash your application.

Comments
  • Version Packages

    Version Packages

    This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

    Releases

    [email protected]

    Minor Changes

    • 11b9e69: improve bundle size
    opened by ivanhofer 1
  • Version Packages

    Version Packages

    This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

    Releases

    [email protected]

    Patch Changes

    • ddcef81: make Types resolve to a more readable name
    opened by ivanhofer 1
  • Version Packages

    Version Packages

    This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

    Releases

    [email protected]

    Patch Changes

    • b987413: update Types to reflect nested Success and Exception types
    opened by ivanhofer 1
  • Version Packages

    Version Packages

    This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

    Releases

    [email protected]

    Minor Changes

    • e9185b4: allow instances of Exceptionally to be wrapped without loosing it's type
    opened by ivanhofer 1
  • Version Packages

    Version Packages

    This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

    Releases

    [email protected]

    Minor Changes

    • 3d335ba: add utilities to extract types
    opened by ivanhofer 1
  • Version Packages

    Version Packages

    This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

    Releases

    [email protected]

    Minor Changes

    • b31c51a: allow not to pass data
    opened by ivanhofer 1
  • Version Packages

    Version Packages

    This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

    Releases

    [email protected]

    Major Changes

    • 83aa448: write documentation and add examples
    opened by ivanhofer 1
  • Version Packages

    Version Packages

    This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

    Releases

    [email protected]

    Minor Changes

    • 85b512c: fix package exports
    opened by ivanhofer 1
  • Version Packages

    Version Packages

    This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

    Releases

    [email protected]

    Patch Changes

    • fee154b: minor fix
    opened by ivanhofer 1
  • Version Packages

    Version Packages

    This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

    Releases

    [email protected]

    Minor Changes

    • 6e41f9f: implement basics
    opened by ivanhofer 1
  • Version Packages

    Version Packages

    This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

    Releases

    [email protected]

    Minor Changes

    • 6e41f9f: implement basics
    opened by github-actions[bot] 0
Owner
Hofer Ivan
passion for modern technologies, TypeScript, Svelte, innovative UI/UX concepts and performance optimizations
Hofer Ivan
A fully type-safe and lightweight internationalization library for all your TypeScript and JavaScript projects.

?? typesafe-i18n A fully type-safe and lightweight internationalization library for all your TypeScript and JavaScript projects. Advantages ?? lightwe

Hofer Ivan 1.3k Jan 4, 2023
Framework agnostic CLI tool for routes parsing and generation of a type-safe helper for safe route usage. ๐Ÿ—บ๏ธ Remix driver included. ๐ŸคŸ

About routes-gen is a framework agnostic CLI tool for routes parsing and generation of a type-safe helper for safe route usage. Think of it as Prisma,

Stratulat Alexandru 192 Jan 2, 2023
100% type-safe query builder for node-postgres :: Generated types, call any function, tree-shakable, implicit type casts, and more

โš ๏ธ This library is currently in alpha. Contributors wanted! tusken Postgres client from a galaxy far, far away. your database is the source-of-truth f

alloc 54 Dec 29, 2022
A zero-dependency, buildless, terse, and type-safe way to write HTML in JavaScript.

hdot A sensible way to write HTML in JavaScript. Type-safe. Helps you follow the HTML spec. Terse. Almost character for character with plain HTML. Bui

Will Martin 31 Oct 24, 2022
Quo is a (free) debugging companion app to help you debug dumped variables, the dumped variables will appear in this Quo client instead of the traditional way which is often tedious.

Quo is a debugging companion to help you debug dumped variables, the dumped variables will appear in this Quo client instead of via the traditional way which is often tedious.

Protoqol 33 Dec 25, 2022
Cloudy is a set of constructs for the AWS Cloud Development Kit that aim to improve the DX by providing a faster and type-safe code environment.

cloudy-ts These packages aren't yet published on npm. This is still highly experimental. Need to figure out a few things before releasing the first ve

Cristian Pallarรฉs 5 Nov 3, 2022
A script that combines a folder of SVG files into a single sprites file and generates type definitions for safe usage.

remix-sprites-example A script that combines a folder of .svg files into a single sprites.svg file and type definitions for safe usage. Technical Over

Nicolas Kleiderer 19 Nov 9, 2022
A functional, immutable, type safe and simple dependency injection library inspired by angular.

func-di English | ็ฎ€ไฝ“ไธญๆ–‡ A functional, immutable, type safe and simple dependency injection library inspired by Angular. Why func-di Installation Usage

null 24 Dec 11, 2022
Type safe library for interacting with Mindbody's Public API (v6) and Webhooks

Mindbody API Type safe library for interacting with Mindbody's Public API (v6) and Webhooks โš ๏ธ Read before installing This library is typed according

SplitPass 4 Dec 9, 2022
Build type-safe web apps with PureScript.

PUX Build type-safe web applications with PureScript. Documentation | Examples | Chat Pux is a PureScript library for building web applications. Inter

Alex Mingoia 567 Jun 18, 2022
Type Safe Object Notation & Validation

tson Type Safe Object Notation & Validation ?? Work in Progress, not ready for production... Features ?? Functional ?? Immutable โœ… Well tested Why? Af

null 9 Aug 10, 2022
Zero runtime type-safe CSS in the same file as components

macaron comptime-css is now called macaron! macaron is a zero-runtime and type-safe CSS-in-JS library made with performance in mind Powered by vanilla

Mokshit Jain 205 Jan 4, 2023
A next-gen framework for type-safe command-line applications

Zors ?? Next-gen framework for building modern, type-safe command-line applications. ?? Tiny (zero dependencies) ?? Runtime agonistic (supports both D

Sidharth Rathi 13 Dec 1, 2022
A compiled-away, type-safe, readable RegExp alternative

?? magic-regexp A compiled-away, type-safe, readable RegExp alternative โœจ Changelog ?? Documentation โ–ถ๏ธ Online playground Features Runtime is zero-dep

Daniel Roe 1.5k Jan 8, 2023
๐Ÿ‘ฉโ€๐ŸŽค Headless, type-safe, UI components for the next generation Web3.Storage APIs.

Headless, type-safe, UI components for the next generation Web3.Storage APIs. Documentation beta.ui.web3.storage Examples React Sign up / Sign in Sing

Web3 Storage 47 Dec 22, 2022
Type-safe session for all Astro SSR project

Astro Session Why use Astro Session? When building server application with Astro, you will often need session system to identify request coming from t

Steven Yung 8 Dec 19, 2022
Catalogist is the easy way to catalog and make your software and (micro)services visible to your organization in a lightweight and developer-friendly way.

catalogist ?? ?? ?? ?? ?? The easy way to catalog and make your software and (micro)services visible to your organization through an API You were a pe

Mikael Vesavuori 11 Dec 13, 2022
Combine type and value imports using Typescript 4.5 type modifier syntax

type-import-codemod Combines your type and value imports together into a single statement, using Typescript 4.5's type modifier syntax. Before: import

Ian VanSchooten 4 Sep 29, 2022
Pure JavaScript library that add .is-hover class instead of css :hover for mobile and desktop.

MultiDeviceHover Pure JavaScript library that add .is-hover class instead of css :hover for mobile and desktop. Usage Install Using npm, install multi

Ryuta Sakai 1 Oct 5, 2021