├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .prettierrc ├── Changelog.md ├── LICENSE ├── README.md ├── build.ts ├── docs ├── README.md └── content │ ├── contents.mdx │ ├── getting-started │ ├── either.mdx │ ├── getting-started.mdx │ ├── makeMightFail.mdx │ ├── makeMightFailSync.mdx │ ├── mightFail.mdx │ ├── mightFailSync.mdx │ └── try-catch-finally-is-bad.mdx │ ├── home.mdx │ ├── jsdocs │ ├── functions │ │ ├── Fail.mdx │ │ ├── Might.mdx │ │ ├── mightFail.mdx │ │ └── mightFailSync.mdx │ ├── index.mdx │ ├── type-aliases │ │ └── Either.mdx │ └── variables │ │ └── default.mdx │ └── more-things │ ├── might-and-fail.mdx │ └── static-methods.mdx ├── examples ├── makeMightFailBasic.ts ├── mightAndFail.ts ├── mightFailBasic.ts ├── mightFailBasicGo.ts ├── mightFailBasicTuple.ts ├── mightFailStaticMethods.ts └── tsconfig.json ├── jsr.json ├── package-lock.json ├── package.cjs.json ├── package.json ├── src ├── Either.ts ├── go │ ├── Either.ts │ ├── index.ts │ ├── makeMightFail.ts │ └── mightFail.ts ├── index.ts ├── makeMightFail.ts ├── mightFail.ts └── utils │ ├── createEither.ts │ ├── errors.ts │ ├── mightFailFunction.ts │ ├── staticMethodsProxy.ts │ └── utils.type.ts ├── test ├── go │ ├── makeMightFail.test.ts │ ├── makeMightFailSync.test.ts │ ├── mightFail.test.ts │ └── mightFailSync.test.ts ├── handleError.test.ts ├── makeMightFail.test.ts ├── makeMightFailSync.test.ts ├── mightFail.test.ts ├── mightFailSync.test.ts └── tuple │ ├── makeMightFail.test.ts │ ├── makeMightFailSync.test.ts │ ├── mightFail.test.ts │ └── mightFailSync.test.ts ├── tsconfig.build.json ├── tsconfig.cjs.json ├── tsconfig.json └── typedoc.json /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish JSR 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | id-token: write # The OIDC ID token is used for authentication with JSR. 14 | steps: 15 | - uses: actions/checkout@v4 16 | - run: npx jsr publish -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | package-lock.json 133 | .idea/ 134 | 135 | docs/push.sh -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": false, 5 | "printWidth": 120, 6 | "endOfLine": "lf", 7 | "trailingComma": "none" 8 | } 9 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [0.7.4] - 2025-02-27 6 | 7 | ### Added 8 | 9 | #### `build.ts` 10 | - .cjs extension to CommonJS output files 11 | - a plugin to fix require() statements to reference .cjs files 12 | 13 | ### Changed 14 | - The build process for npm uses a build.ts file to build the project with esbuild. This gives esm js files the .js extension which is needed for some environments. 15 | 16 | 17 | ## [0.7.3] - 2024-12-11 18 | 19 | ### Changed 20 | - Fixed the error type to make it more generic and consistent with TS: https://github.com/might-fail/ts/issues/20 21 | 22 | ## [0.7.2] - 2024-11-30 23 | 24 | ### Changed 25 | - Removed `NotUndefined` type. https://github.com/might-fail/ts/issues/20 26 | 27 | ## [0.7.0] - 2024-10-27 28 | 29 | 30 | ### Changed 31 | - The either type now accepts type without promise and returns the awaited type. 32 | 33 | ## [0.6.3] - 2024-10-6 34 | 35 | ### Added 36 | - `build.ts` to build the project with esbuild 37 | 38 | ### Changed 39 | - the build process for npm uses a build.ts file to build the project with esbuild. This gives esm js files the .js extension which is needed for some environments. 40 | 41 | 42 | ## [0.6.1] - 2024-10-6 43 | 44 | ### Added 45 | - `Might` and `Fail` functions 46 | - new docs page in the readme 47 | 48 | ### Changed 49 | - `mightFail` and `mightFailSync` now return an object tuple so you can destructure it as an object or a tuple, your choice. 50 | - removed the `/tuple` folder since the normal `might-fail` import will work the same. 51 | 52 | 53 | 54 | ## [0.5.0] - 2024-10-2 55 | 56 | ### Added 57 | - `mightFail.any` support 58 | - `mightFail.race` support 59 | - `mightFail.all` support 60 | - `mightFail.allSettled` support 61 | 62 | 63 | ## [0.4.0] - 2024-09-29 64 | 65 | ### Added 66 | - `/go` folder for a Go-style tuple implementation. `const [result, error] = mightFail(promise)` 67 | - `/tuple` folder for a tuple implementation. `const [error, result] = mightFail(promise)` 68 | 69 | ### Changed 70 | - If a function throws a string, the string will be the message of the new Error object. 71 | - If a function throws an object with a message property, the message will be the message of the new Error object. 72 | - If a function throws an object it will be passed to the Error object constructor as is. 73 | 74 | ## [0.3.0] - 2024-03-16 75 | 76 | ### Added 77 | - Support for synchronous error handling with `mightFailSync` and `makeMightFailSync` functions. 78 | - A new `publish` script in `package.json` to streamline the build and publish process. 79 | 80 | ### Changed 81 | - The library now officially supports both async and sync error handling. This change is reflected in the README to emphasize the library's versatility in handling errors in different contexts. 82 | - Updated `Either.ts` to streamline the type definition for a more straightforward implementation. 83 | 84 | ## [0.1] - [0.2] 85 | 86 | 87 | ### Added 88 | - Initial support for async error handling with `mightFail` and `makeMightFail` functions. 89 | - Comprehensive documentation in the README, illustrating the use of the library with practical examples. 90 | - Implementation of the `Either` type to support the async error handling pattern. 91 | 92 | ### Changed 93 | - Various internal improvements for better performance and reliability. 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sam Meech-Ward 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Might Fail 2 | 3 | A TypeScript library for handling async and sync errors without `try` and `catch` blocks. Inspired by other languages that utilize Result or Either types for safer error handling. 4 | 5 | This works for sync and async code, and you can choose the error handling style that you like. 6 | 7 | Scroll to the bottom for the motivation section (why `try,catch,finally` blocks are bad). 8 | 9 | ## Docs 10 | 11 | [https://mightfail.dev](https://mightfail.dev) 12 | 13 | ## Install 14 | 15 | [![JSR](https://jsr.io/badges/@might/fail)](https://jsr.io/@might/fail) 16 | [![npm version](https://img.shields.io/npm/v/might-fail.svg)](https://www.npmjs.com/package/might-fail) 17 | 18 | 19 | ## Style 20 | 21 | There's three different styles to choose from and they all work the same way, but you can choose your favorite API. 22 | 23 | All examples are in the default style, but you **can** use any of the three styles. 24 | 25 | 26 | ### Tuple Style 27 | 28 | ```ts 29 | import { mightFail } from "might-fail"; // from "@might/fail" 30 | 31 | const [error, result] = await mightFail(promise); 32 | ``` 33 | 34 | ### Object Style 35 | 36 | ```ts 37 | import { mightFail } from "might-fail"; // from "@might/fail" 38 | 39 | const { error, result } = await mightFail(promise); 40 | ``` 41 | 42 | ### Go Style 43 | 44 | ```ts 45 | import { mightFail } from "might-fail/go"; // from "@might/fail/go" 46 | 47 | const [result, error] = await mightFail(promise); 48 | ``` 49 | 50 | 51 | ## Async 52 | 53 | ### Wrap Promise in `mightFail` 54 | 55 | ```ts 56 | const [ error, result ] = await mightFail(axios.get("/posts")); 57 | 58 | if (error) { 59 | // handle error 60 | return; 61 | } 62 | 63 | const posts = result.data 64 | posts.map((post) => console.log(post.title)); 65 | ``` 66 | 67 | **or:** 68 | 69 | ```ts 70 | const [ networkError, result ] = await mightFail(fetch("/posts")); 71 | 72 | if (networkError) { 73 | // handle network error 74 | return; 75 | } 76 | 77 | if (!result.ok) { 78 | // handle an error response from server 79 | return; 80 | } 81 | 82 | const [ convertToJSONError, posts ] = await mightFail( 83 | result.json() 84 | ); 85 | 86 | if (convertToJSONError) { 87 | // handle convertToJSONError 88 | return; 89 | } 90 | 91 | posts.map((post) => console.log(post.title)); 92 | ``` 93 | 94 | ### Or Wrap Async Function in `makeMightFail` 95 | 96 | ```ts 97 | const get = makeMightFail(axios.get); 98 | const [ error, result ] = await get("/posts"); 99 | 100 | if (error) { 101 | // handle error 102 | return; 103 | } 104 | 105 | const posts = result.data 106 | posts.map((post) => console.log(post.title)); 107 | ``` 108 | 109 | ## Sync 110 | 111 | 112 | ### Wrap throwing functions in `mightFailSync` 113 | 114 | ```ts 115 | const [ error, result ] = mightFailSync(() => JSON.parse("")); // JSON.parse might throw 116 | if (error) { 117 | console.error('Parsing failed:', error); 118 | return 119 | } 120 | console.log('Parsed object:', result); 121 | ``` 122 | 123 | ### Or Wrap Sync Function in `makeMightFailSync` 124 | 125 | ```ts 126 | function parseJSON(jsonString: string) { 127 | return JSON.parse(jsonString); // This might throw 128 | } 129 | const safeParseJSON = makeMightFailSync(parseJSON); 130 | 131 | const [ error, result ] = safeParseJSON(""); 132 | 133 | if (error) { 134 | console.error("Parsing failed:", error); 135 | return; 136 | } 137 | console.log("Parsed object:", result); 138 | ``` 139 | 140 | --- 141 | 142 | ## Either Type 143 | 144 | `await`ing the `mightFail` functions will return an `Either` type with either an `error` or a `result`. 145 | 146 | - `error` **always** has the type `Error | undefined`. 147 | - If an instance of `Error` is not thrown, then a new `Error` will be created from the thrown value. 148 | - `result` always has the type `T | undefined` where `T` is the type of the result of the promise passed to `mightFail`. 149 | 150 | This means that the you never lose the type information of the result of the promise passed to `mightFail`. 151 | 152 | The structure of the `Either` type can be "standard", "tuple", or "go" which you can choose based on your preference. Standard is the default for now, typle and go can be used by importing mightfail from the `tuple` or `go` subdirectory. 153 | 154 | - `standard` - `{ error: Error | undefined, result: T | undefined }` 155 | - `tuple` - `[Error | undefined, T | undefined]` 156 | - `go` - `[T | undefined, Error | undefined]` 157 | 158 | --- 159 | 160 | ## Static Methods 161 | 162 | `mightFail` has static methods that wrap the corresponding static methods of `Promise`. 163 | 164 | - [`Promise.race`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race) 165 | - [`Promise.all`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all) 166 | - [`Promise.any`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any) 167 | 168 | 169 | - `await mightFail.all([])` 170 | - `await mightFail.race([])` 171 | - `await mightFail.any([])` 172 | 173 | These are identical to the static methods on `Promise` but they return an `Either` type. 174 | 175 | --- 176 | 177 | ## Motivation 178 | 179 | I think throwing exceptions is cool, I like that an exception breaks control flow and I like exception propogation. The only thing I don't like catching exceptions. This mostly happens at the most "user facing" part of the code like an api endpoint or a UI component, the outer most function call. So catching an exception needs to notify the user that something went wrong, log the error for debugging, and stop the currently execution flow. 180 | 181 | ### Guard ✅ 182 | 183 | Guarding allows you to handle your errors early and return from the function early, making them more readable and easier to reason about. 184 | 185 | ```ts 186 | const { error: networkError, result } = await mightFail(fetch("/posts")); 187 | // guard against a network error 188 | if (networkError) { 189 | return; 190 | } 191 | // guard against an error response from the server 192 | if (!result.ok) { 193 | return; 194 | } 195 | const { error: convertToJSONError, result: posts } = await mightFail( 196 | result.json() 197 | ); 198 | // guard against an error converting the response to JSON 199 | if (convertToJSONError) { 200 | return; 201 | } 202 | 203 | // success case, unnested and at the bottom of the function 204 | posts.map((post) => console.log(post.title)); 205 | ``` 206 | 207 | The success case is now the only code that is not nested in an `if` statement. It's also at the very bottom of the function making it easy to find. 208 | 209 | ### Everything in One Try/Catch Block ❌ 210 | 211 | ```ts 212 | try { 213 | const response = await fetch("/posts"); 214 | 215 | if (!response.ok) { 216 | // handle an error response from server 217 | return; 218 | } 219 | const posts = await response.json(); 220 | 221 | posts.map((post) => console.log(post.title)); 222 | } catch (error) { 223 | // handle any errors, not sure which one though 🤷‍♀️ 224 | } 225 | ``` 226 | 227 | This is bad because: 228 | 229 | - Error handling happens in multiple places in the function. 230 | - The catch block will catch **any** and **all** errors which makes it difficult to handle different errors differently. 231 | - All the success case code will happen inside of the try block 232 | 233 | ### Multiple Try/Catch Blocks ❌ 234 | 235 | ```ts 236 | let response: Response; 237 | try { 238 | response = await fetch("/posts"); 239 | } catch (error) { 240 | // guard against a network error 241 | return; 242 | } 243 | if (!response.ok) { 244 | // guard against an error response from server 245 | return; 246 | } 247 | 248 | let posts: Post[]; 249 | try { 250 | posts = await response.json(); 251 | } catch (error) { 252 | // guard against an error converting the response to JSON 253 | return; 254 | } 255 | 256 | posts.map((post) => console.log(post.title)); 257 | ``` 258 | 259 | Declaring the variable ahead of time is a little weird and it makes infering the type of the variable a little more difficult. Also, try catch finally blocks can be confusing. 260 | 261 | ### `try` `catch` `finally` can be confusing ❌ 262 | 263 | ```ts 264 | function something() { 265 | try { 266 | throw new Error("something went wrong"); 267 | } catch(error) { 268 | console.log("error happened") 269 | return "error return" 270 | } finally { 271 | console.log("finally happened") 272 | return "finally return" 273 | } 274 | return "something return" 275 | } 276 | console.log(something()) 277 | ``` 278 | 279 | Can every single dev in your team understand what the above code will print out? 280 | -------------------------------------------------------------------------------- /build.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This script was taken from hono https://github.com/honojs/hono/blob/main/build.ts 3 | Everything below is theres 4 | */ 5 | /* 6 | This script is heavily inspired by `built.ts` used in @kaze-style/react. 7 | https://github.com/taishinaritomi/kaze-style/blob/main/scripts/build.ts 8 | MIT License 9 | Copyright (c) 2022 Taishi Naritomi 10 | */ 11 | 12 | import { exec } from "child_process" 13 | import fs from "fs" 14 | import path from "path" 15 | import { build } from "esbuild" 16 | import type { Plugin, PluginBuild, BuildOptions } from "esbuild" 17 | import glob from "glob" 18 | 19 | // Plugin to fix CommonJS imports by appending .cjs to require statements 20 | const fixCjsImportsPlugin = (): Plugin => ({ 21 | name: "fix-cjs-imports", 22 | setup(build: PluginBuild) { 23 | // Run in the onEnd hook after all files have been written 24 | build.onEnd((result) => { 25 | // Only proceed if the build is successful 26 | if (result.errors.length === 0) { 27 | // Get the output directory from the build options 28 | const outdir = build.initialOptions.outdir 29 | if (!outdir) return 30 | 31 | // Find all .cjs files in the output directory 32 | const files = glob.sync(`${outdir}/**/*.cjs`) 33 | 34 | files.forEach((file) => { 35 | let content = fs.readFileSync(file, "utf8") 36 | 37 | // Replace all require('./something') with require('./something.cjs') 38 | content = content.replace(/require\(["'](\.[^"']+)["']\)/g, (match, importPath) => { 39 | // Don't add .cjs if it already has an extension 40 | if (path.extname(importPath) !== "") { 41 | return match 42 | } 43 | return `require('${importPath}.cjs')` 44 | }) 45 | 46 | fs.writeFileSync(file, content) 47 | }) 48 | } 49 | }) 50 | } 51 | }) 52 | 53 | const entryPoints = glob.sync("./src/**/*.ts", { 54 | ignore: ["./src/**/*.test.ts", "./src/mod.ts", "./src/middleware.ts", "./src/deno/**/*.ts"] 55 | }) 56 | 57 | /* 58 | This plugin is inspired by the following. 59 | https://github.com/evanw/esbuild/issues/622#issuecomment-769462611 60 | */ 61 | const addExtension = (extension: string = ".js", fileExtension: string = ".ts"): Plugin => ({ 62 | name: "add-extension", 63 | setup(build: PluginBuild) { 64 | build.onResolve({ filter: /.*/ }, (args) => { 65 | if (args.importer) { 66 | const p = path.join(args.resolveDir, args.path) 67 | let tsPath = `${p}${fileExtension}` 68 | 69 | let importPath = "" 70 | if (fs.existsSync(tsPath)) { 71 | importPath = args.path + extension 72 | } else { 73 | tsPath = path.join(args.resolveDir, args.path, `index${fileExtension}`) 74 | if (fs.existsSync(tsPath)) { 75 | if (args.path.endsWith("/")) { 76 | importPath = `${args.path}index${extension}` 77 | } else { 78 | importPath = `${args.path}/index${extension}` 79 | } 80 | } 81 | } 82 | return { path: importPath, external: true } 83 | } 84 | }) 85 | } 86 | }) 87 | 88 | const commonOptions: BuildOptions = { 89 | entryPoints, 90 | logLevel: "info", 91 | platform: "node", 92 | tsconfig: "tsconfig.json", 93 | target: "es2022" 94 | } 95 | 96 | const cjsBuild = () => 97 | build({ 98 | ...commonOptions, 99 | outbase: "./src", 100 | outdir: "./dist/cjs", 101 | format: "cjs", 102 | outExtension: { ".js": ".cjs" }, 103 | plugins: [fixCjsImportsPlugin()], 104 | tsconfig: "tsconfig.cjs.json" 105 | }) 106 | 107 | const esmBuild = () => 108 | build({ 109 | ...commonOptions, 110 | bundle: true, 111 | outbase: "./src", 112 | outdir: "./dist", 113 | format: "esm", 114 | plugins: [addExtension(".js")] 115 | }) 116 | 117 | Promise.all([esmBuild(), cjsBuild()]) 118 | 119 | exec(`tsc --emitDeclarationOnly --declaration --project tsconfig.build.json`) 120 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Might Fail Docs 2 | 3 | ### Blocks 4 | 5 | #### YouTube 6 | 7 | ```mdx 8 | 9 | ``` 10 | 11 | #### Example Block 12 | 13 | ```mdx 14 | 15 | 16 | Return a single root element, div or empty tag `<>` 17 | 18 | \`\`\`some-component.jsx 19 | // focus(1:2) 20 | return ( 21 |
22 |

Hello

23 |

World

24 | random 25 |
26 | ) 27 | \`\`\` 28 |
29 | ``` 30 | 31 | #### Instruction Block 32 | 33 | ```mdx 34 | 35 | 36 | Add this code to your thing: 37 | 38 | 39 | 40 | ```App.js 41 | return ( 42 |
43 |

Hello

44 |

World

45 |

{new Date().toString()}

46 |
47 | ) 48 | ``` 49 |
50 |
51 | ``` 52 | 53 | ```mdx 54 | 55 | type Ratio = "1" | "1:1" | "2:3" | "3:2" | "3:5" | "5:3" | "4:7" | "1:2" | "1:0" | "0:1" 56 | ``` 57 | 58 | #### Tabs 59 | 60 | ``` 61 | 62 | 63 | ```sh 64 | npx create-next-app@latest 65 | ``` 66 | 67 | 68 | ```sh 69 | yarn create next-app 70 | ``` 71 | 72 | 73 | ```sh 74 | pnpm create next-app 75 | ``` 76 | 77 | 78 | ```sh 79 | bunx create-next-app 80 | ``` 81 | 82 | 83 | ``` 84 | 85 | ## Card 86 | 87 | ``` 88 | 89 | 90 | ``` 91 | 92 | ## File Tree 93 | 94 | ```mdx 95 | 96 | 97 | - src 98 | - app 99 | - page.tsx 100 | 101 | 102 | ``` 103 | 104 | ```mdx 105 | 106 | 107 | Create a new file inside of `create-post` called `page.tsx`. 108 | 109 | 110 | 111 | 112 | - src 113 | - app 114 | - page.tsx 115 | - create-post (`/create-post` route) 116 | - page.tsx (UI) 117 | 118 | 119 | 120 | 121 | ``` 122 | 123 | ## Code 124 | 125 | ```mdx 126 | 127 | ```users.ts 128 | import { pgTable, text, varchar, timestamp } from "drizzle-orm/pg-core" 129 | 130 | export const users = pgTable("users", { 131 | id: text("id").primaryKey(), 132 | username: varchar("username", { length: 30 }).notNull(), 133 | firstName: varchar("first_name", { length: 50 }).notNull(), 134 | lastName: varchar("last_name", { length: 50 }).notNull(), 135 | avatar: text("avatar").notNull(), 136 | createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), 137 | }) 138 | ``` 139 | ```sql 140 | CREATE TABLE IF NOT EXISTS "users" ( 141 | "id" text PRIMARY KEY NOT NULL, 142 | "username" varchar(30) NOT NULL, 143 | "first_name" varchar(50) NOT NULL, 144 | "last_name" varchar(50) NOT NULL, 145 | "avatar" text NOT NULL, 146 | "created_at" timestamp with time zone DEFAULT now() NOT NULL 147 | ); 148 | ``` 149 | 150 | ``` 151 | 152 | 153 | ```mdx 154 | 155 | ```/home/ubuntu/index.html 156 | 157 | 158 | 159 | 160 | Hello World 161 | 162 | 163 |

Hello World

164 |

🌎

165 | 166 | 167 | ``` 168 | ```/home/ubuntu/404.html 169 | 170 | 171 | 172 | 173 | 404 - Page Not Found 174 | 175 | 176 |

404 - Page not found

177 |

🤷‍♀️

178 | 179 | 180 | ``` 181 |
182 | ``` 183 | 184 | **Not yet fully working:** 185 | ```mdx 186 | 187 | ```users.ts 188 | // toolbar sql 189 | import { text, varchar, pgTable, timestamp } from "drizzle-orm/pg-core" 190 | 191 | export const users = pgTable("users", { 192 | id: text("id").primaryKey(), 193 | username: varchar("username", { length: 30 }).notNull(), 194 | firstName: varchar("first_name", { length: 50 }).notNull(), 195 | lastName: varchar("last_name", { length: 50 }).notNull(), 196 | avatar: text("avatar").notNull(), 197 | createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), 198 | }) 199 | ``` 200 | ```schema.sql 201 | -- toolbar sql 202 | CREATE TABLE IF NOT EXISTS "users" ( 203 | "id" text PRIMARY KEY NOT NULL, 204 | "username" varchar(30) NOT NULL, 205 | "first_name" varchar(50) NOT NULL, 206 | "last_name" varchar(50) NOT NULL, 207 | "avatar" text NOT NULL, 208 | "created_at" timestamp with time zone DEFAULT now() NOT NULL 209 | ); 210 | ``` 211 | 212 | ``` 213 | 214 | ## Image 215 | 216 | The image path needs to be aboslute to where it is in the markdown system. 217 | append dark or light to the end of the image path to switch between the two. 218 | 219 | ```md 220 | ![Create Project](/part-2/10-setup-neon-images/project-creation-dark.png) ![Create Project](/part-2/10-setup-neon-images/project-creation-light.png) 221 | ``` -------------------------------------------------------------------------------- /docs/content/contents.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | course: 3 | title: "Might Fail" 4 | parts: 5 | - title: "Getting Started" 6 | slug: /getting-started 7 | filePath: /getting-started/getting-started.mdx 8 | icon: "rocket" 9 | pages: 10 | - title: "❌ try, catch, finally" 11 | filePath: /getting-started/try-catch-finally-is-bad.mdx 12 | slug: /try-catch-finally-is-bad 13 | type: documentation 14 | - title: "`mightFail`" 15 | filePath: /getting-started/mightFail.mdx 16 | slug: /might-fail 17 | type: documentation 18 | - title: "`makeMightFail`" 19 | filePath: /getting-started/makeMightFail.mdx 20 | slug: /make-might-fail 21 | type: documentation 22 | - title: "`mightFailSync`" 23 | filePath: /getting-started/mightFailSync.mdx 24 | slug: /might-fail-sync 25 | type: documentation 26 | - title: "`makeMightFailSync`" 27 | filePath: /getting-started/makeMightFailSync.mdx 28 | slug: /make-might-fail-sync 29 | type: documentation 30 | - title: "Either Type" 31 | filePath: /getting-started/either.mdx 32 | slug: /either 33 | type: documentation 34 | - title: "More Things" 35 | icon: "rocket" 36 | pages: 37 | - title: "Static Methods" 38 | filePath: /more-things/static-methods.mdx 39 | slug: /static-methods 40 | type: documentation 41 | - title: "`might-and-fail`" 42 | filePath: /more-things/might-and-fail.mdx 43 | slug: /might-and-fail 44 | type: documentation 45 | - title: "API Reference" 46 | slug: /api-reference 47 | filePath: /getting-started/jsdocs/variables/default.mdx 48 | icon: "file-text" 49 | pages: 50 | - title: "`mightFail`" 51 | filePath: /jsdocs/functions/mightFail.mdx 52 | slug: /functions/mightFail 53 | type: documentation 54 | - title: "`mightFailSync`" 55 | filePath: /jsdocs/functions/mightFailSync.mdx 56 | slug: /functions/mightFailSync 57 | type: documentation 58 | - title: "`Either`" 59 | filePath: /jsdocs/type-aliases/Either.mdx 60 | slug: /type-aliases/Either 61 | type: documentation 62 | - title: "`Fail`" 63 | filePath: /jsdocs/functions/Fail.mdx 64 | slug: /functions/Fail 65 | type: documentation 66 | - title: "`Might`" 67 | filePath: /jsdocs/functions/Might.mdx 68 | slug: /functions/Might 69 | type: documentation 70 | --- 71 | -------------------------------------------------------------------------------- /docs/content/getting-started/either.mdx: -------------------------------------------------------------------------------- 1 | # Either Type 2 | 3 | The returned value from `await mightFail` or `mightFailSync` is an `Either` type. This type has **either** an `error` or a `result`, never both. 4 | 5 | ## Error 6 | 7 | If an error is thrown from the wrapped function, then the `Either` result will only contain an `error`. 8 | 9 | 10 | 11 | ```ts 12 | const [error, result] = await mightFail(Promise.reject("oh no")); 13 | // error is an instance of Error with the message "oh no" 14 | // result is undefined 15 | ``` 16 | 17 | 18 | 19 | ```ts 20 | const [error, result] = mightFailSync(() => throw "oh no"); 21 | // error is an instance of Error with the message "oh no" 22 | // result is undefined 23 | ``` 24 | 25 | 26 | 27 | The wrapped functions will hopefully throw an error object, and your own code will hopefully throw an error object. However, if something else is thrown, then a new `Error` will be created from the thrown value. 28 | 29 | So no more `if (error instanceof Error) { ... }` checks! 30 | 31 | ## Result 32 | 33 | If the wrapped function completes successfully, then the `Either` result will only contain a `result`. 34 | 35 | 36 | 37 | ```ts 38 | const [error, result] = await mightFail(Promise.resolve("success")); 39 | // error is undefined 40 | // result is the result of the promise ("success" in this case) 41 | ``` 42 | 43 | 44 | 45 | ```ts 46 | const [error, result] = mightFailSync(() => "success"); 47 | // error is undefined 48 | // result is the result of the promise and all 49 | ``` 50 | 51 | 52 | 53 | 54 | ## Type safety 55 | 56 | You **never** lose the type information of the result of the promise passed to `mightFail`. As long as there is no error, the result will be whatever type was resolved from the promise or returned from the sync function. 57 | 58 | ```ts 59 | async function example() { 60 | if (Math.random() > 0.5) { 61 | throw new Error("oh no"); 62 | } 63 | return { example: "success" }; 64 | } 65 | 66 | const [error, result] = await mightFail(example()); 67 | // error: Error | undefined 68 | // result: { example: string } | undefined 69 | if (error) { 70 | // error: Error 71 | return; 72 | } 73 | // result: { example: string } 74 | console.log(result.example); 75 | ``` 76 | 77 | ## Structure 78 | 79 | The structure of the `Either` type is a tuple or an object. Or a "go" tuple if you import from `/go` 80 | 81 | ```ts 82 | const [error, result] = await mightFail(example()); 83 | // or 84 | const {error, result} = await mightFail(example()); 85 | // or 86 | import { mightFail } from 'might-fail/go' 87 | const [result, error] = await mightFail(example()); 88 | ``` 89 | 90 | No matter which structure you choose, the way you use the `Either` type is the same. 91 | -------------------------------------------------------------------------------- /docs/content/getting-started/getting-started.mdx: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | * [❌ try, catch, finally](/try-catch-finally-is-bad) 4 | * [`mightFail`](/might-fail) 5 | * [`mightFailSync`](/might-fail-sync) 6 | * [Static Methods](/static-methods) 7 | * [Either Type](/either) 8 | * [Might & Fail](/might-and-fail) 9 | 10 | ## Change the way you catch errors 11 | 12 | That's it, that's all this library helps you do. No major re-writes for your code base, no major changes at all. Just start handling your errors in a **much** better way. 13 | 14 | You still throw errors or let other libraries throw errors. Just stop using `try, catch, finally` and use `mightFail` or `mightFailSync` instead. 15 | 16 | ## `catch` Sucks. Guarding is Good. 17 | 18 | Guarding allows you to handle your errors early and return from the function early, making them more readable and easier to reason about. 19 | 20 | ```ts 21 | const [ networkError, result ] = await mightFail(fetch("/posts")); 22 | // guard against a network error 23 | if (networkError) { 24 | return; 25 | } 26 | // guard against an error response from the server 27 | if (!result.ok) { 28 | return; 29 | } 30 | const [ convertToJSONError, posts ] = await mightFail( 31 | result.json() 32 | ); 33 | // guard against an error converting the response to JSON 34 | if (convertToJSONError) { 35 | return; 36 | } 37 | 38 | // success case, unnested and at the bottom of the function 39 | posts.map((post) => console.log(post.title)); 40 | ``` 41 | 42 | The success case is now the only code that is **not** nested in another block. It's also at the very bottom of the function making it easy to find. 43 | 44 | 45 | The sucess case is always at the bottom of the function. All of your error handling logic is next to where the error might occur. 46 | 47 | -------------------------------------------------------------------------------- /docs/content/getting-started/makeMightFail.mdx: -------------------------------------------------------------------------------- 1 | ## `makeMightFail` 2 | 3 | You also have the option to make your own `mightFail` wrapper function. Pass in any async function to `makeMightFail` and it will return a `mightFail` wrapper function for you to use. 4 | 5 | 6 | This is useful in some use cases, but most of the time you should just use `mightFail` directly. 7 | 8 | 9 | 10 | 11 | ```ts 12 | const get = makeMightFail(axios.get); 13 | const [ error, result ] = await get("/posts"); 14 | 15 | if (error) { 16 | // handle error 17 | return; 18 | } 19 | 20 | const posts = result.data 21 | posts.map((post) => console.log(post.title)); 22 | ``` 23 | 24 | 25 | ```ts 26 | const get = makeMightFail(axios.get); 27 | const { error, result } = await get("/posts"); 28 | 29 | if (error) { 30 | // handle error 31 | return; 32 | } 33 | 34 | const posts = result.data 35 | posts.map((post) => console.log(post.title)); 36 | ``` 37 | 38 | 39 | ```ts 40 | const get = makeMightFail(axios.get); 41 | const [ result, error ] = await get("/posts"); 42 | 43 | if (error) { 44 | // handle error 45 | return; 46 | } 47 | 48 | const posts = result.data 49 | posts.map((post) => console.log(post.title)); 50 | ``` 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /docs/content/getting-started/makeMightFailSync.mdx: -------------------------------------------------------------------------------- 1 | ## `makeMightFailSync` 2 | 3 | You also have the option to make your own `mightFailSync` wrapper function. Pass in any sync function to `makeMightFailSync` and it will return a `mightFailSync` wrapper function for you to use. 4 | 5 | 6 | This is useful in some use cases, but most of the time you should just use `mightFailSync` directly. 7 | 8 | 9 | 10 | 11 | ```ts 12 | function parseJSON(jsonString: string) { 13 | return JSON.parse(jsonString); // This might throw 14 | } 15 | const mightParseJSON = makeMightFailSync(parseJSON); 16 | 17 | const [error, result] = mightParseJSON(""); 18 | 19 | if (error) { 20 | console.error("Parsing failed:", error); 21 | return; 22 | } 23 | 24 | console.log("Parsed object:", result); 25 | ``` 26 | 27 | 28 | ```ts 29 | function parseJSON(jsonString: string) { 30 | return JSON.parse(jsonString); // This might throw 31 | } 32 | const mightParseJSON = makeMightFailSync(parseJSON); 33 | 34 | const { error, result } = mightParseJSON(""); 35 | 36 | if (error) { 37 | console.error("Parsing failed:", error); 38 | return; 39 | } 40 | 41 | console.log("Parsed object:", result); 42 | ``` 43 | 44 | 45 | ```ts 46 | function parseJSON(jsonString: string) { 47 | return JSON.parse(jsonString); // This might throw 48 | } 49 | const mightParseJSON = makeMightFailSync(parseJSON); 50 | 51 | const [result, error] = mightParseJSON(""); 52 | 53 | if (error) { 54 | console.error("Parsing failed:", error); 55 | return; 56 | } 57 | 58 | console.log("Parsed object:", result); 59 | ``` 60 | 61 | -------------------------------------------------------------------------------- /docs/content/getting-started/mightFail.mdx: -------------------------------------------------------------------------------- 1 | 2 | # `mightFail` Async 3 | 4 | ## Wrap Promise 5 | 6 | Simply wrap any promise in `mightFail` and await the result. 7 | 8 | You will get and `Either` object that contains either an error or the result. Never both. 9 | 10 | ```ts 11 | const eitherObject = await mightFail(fetch("/posts")); 12 | ``` 13 | 14 | But **don't** store the `Either` object directly. Instead, destructure it in the same statement. 15 | 16 | ```ts 17 | // tuple destructuring 18 | const [ error, result ] = await mightFail(fetch("/posts")); 19 | 20 | // object destructuring 21 | const { error, result } = await mightFail(fetch("/posts")); 22 | 23 | // go destructuring 24 | // import { mightFail } from "might-fail/go"; 25 | const [ result, error ] = await mightFail(fetch("/posts")); 26 | ``` 27 | 28 | You can choose to destructure this object as a tuple or as an object. Or as a backwards tuple if you prefer it that way. 29 | 30 | We think that the **tuple** option is the best, but you do you. 31 | 32 | ## Guard 33 | 34 | Once you've awaited and destructured the `Either` object, use guard clauses to handle the error, and handle the success case at the end. 35 | 36 | 37 | 38 | 39 | ```ts 40 | const [ networkError, result ] = await mightFail(fetch("/posts")); 41 | 42 | if (networkError) { 43 | // handle network error 44 | return; 45 | } 46 | 47 | if (!result.ok) { 48 | // handle an error response from server 49 | return; 50 | } 51 | 52 | const [convertToJSONError, posts] = await mightFail( 53 | result.json() 54 | ); 55 | 56 | if (convertToJSONError) { 57 | // handle convertToJSONError 58 | return; 59 | } 60 | 61 | posts.map((post) => console.log(post.title)); 62 | ``` 63 | 64 | 65 | 66 | ```ts 67 | const { error: networkError, result } = await mightFail(fetch("/posts")); 68 | 69 | if (networkError) { 70 | // handle network error 71 | return; 72 | } 73 | 74 | if (!result.ok) { 75 | // handle an error response from server 76 | return; 77 | } 78 | 79 | const { error: convertToJSONError, result: posts } = await mightFail( 80 | result.json() 81 | ); 82 | 83 | if (convertToJSONError) { 84 | // handle convertToJSONError 85 | return; 86 | } 87 | 88 | posts.map((post) => console.log(post.title)); 89 | ``` 90 | 91 | 92 | 93 | ```ts 94 | const [ result, networkError ] = await mightFail(fetch("/posts")); 95 | 96 | if (networkError) { 97 | // handle network error 98 | return; 99 | } 100 | 101 | if (!result.ok) { 102 | // handle an error response from server 103 | return; 104 | } 105 | 106 | const [ posts, convertToJSONError ] = await mightFail( 107 | result.json() 108 | ); 109 | 110 | if (convertToJSONError) { 111 | // handle convertToJSONError 112 | return; 113 | } 114 | 115 | posts.map((post) => console.log(post.title)); 116 | ``` 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /docs/content/getting-started/mightFailSync.mdx: -------------------------------------------------------------------------------- 1 | # mightFailSync 2 | 3 | ## Wrap Throwing Function 4 | 5 | Simply wrap any throwing function in `mightFailSync`. 6 | 7 | You will get and `Either` object that contains either an error or the result. Never both. 8 | 9 | 10 | ```ts 11 | const eitherObject = mightFailSync(() => JSON.parse("")); // JSON.parse might throw 12 | ``` 13 | 14 | But **don't** store the `Either` object directly. Instead, destructure it in the same statement. 15 | 16 | ```ts 17 | // tuple destructuring 18 | const [ error, result ] = mightFailSync(() => JSON.parse("")); 19 | 20 | // object destructuring 21 | const { error, result } = mightFailSync(() => JSON.parse("")); 22 | 23 | // go destructuring 24 | // import { mightFailSync } from "might-fail/go"; 25 | const [ result, error ] = mightFailSync(() => JSON.parse("")); 26 | ``` 27 | 28 | You can choose to destructure this object as a tuple or as an object. Or as a backwards tuple if you prefer it that way. 29 | 30 | We think that the **tuple** option is the best, but you do you. 31 | 32 | ## Guard 33 | 34 | Once you've destructured the `Either` object, use guard clauses to handle the error, and handle the success case at the end. 35 | 36 | 37 | 38 | ```ts 39 | const [error, result] = mightFailSync(() => JSON.parse("")); 40 | if (error) { 41 | // handle the parsing error 42 | return 43 | } 44 | 45 | console.log('Parsed object:', result); 46 | ``` 47 | 48 | 49 | 50 | ```ts 51 | const {error, result} = mightFailSync(() => JSON.parse("")); // JSON.parse might throw 52 | if (error) { 53 | // handle the parsing error 54 | return 55 | } 56 | 57 | console.log('Parsed object:', result); 58 | ``` 59 | 60 | 61 | ```ts 62 | const [result, error] = mightFailSync(() => JSON.parse("")); // JSON.parse might throw 63 | if (error) { 64 | // handle the parsing error 65 | return 66 | } 67 | 68 | console.log('Parsed object:', result); 69 | ``` 70 | 71 | 72 | -------------------------------------------------------------------------------- /docs/content/getting-started/try-catch-finally-is-bad.mdx: -------------------------------------------------------------------------------- 1 | # try, catch, finally is bad 2 | 3 |

try catch blocks are just confusing pic.twitter.com/IFaLOtV2nr

— Sam Meech Ward (@Meech_Ward) October 13, 2024
4 | 5 | I think throwing exceptions is nice, I like that an exception breaks control flow and I like exception propogation. The only thing I don't like catching exceptions. 6 | 7 | This mostly happens at the most "user facing" part of the code like an api endpoint or a UI component, the outer most function call. So catching an exception needs to notify the user that something went wrong, log the error for debugging, and stop the currently execution flow. 8 | 9 | ## Guard ✅ 10 | 11 | Guarding allows you to handle your errors early and return from the function early, making them more readable and easier to reason about. 12 | 13 | ```ts 14 | const [ networkError, result ] = await mightFail(fetch("/posts")); 15 | // guard against a network error 16 | if (networkError) { 17 | return; 18 | } 19 | // guard against an error response from the server 20 | if (!result.ok) { 21 | return; 22 | } 23 | const [ convertToJSONError, posts ] = await mightFail( 24 | result.json() 25 | ); 26 | // guard against an error converting the response to JSON 27 | if (convertToJSONError) { 28 | return; 29 | } 30 | 31 | // success case, unnested and at the bottom of the function 32 | posts.map((post) => console.log(post.title)); 33 | ``` 34 | 35 | The success case is now the only code that is not nested in an `if` statement. It's also at the very bottom of the function making it easy to find. 36 | 37 | ## Everything in One Try/Catch Block ❌ 38 | 39 | ```ts 40 | try { 41 | const response = await fetch("/posts"); 42 | 43 | if (!response.ok) { 44 | // handle an error response from server 45 | return; 46 | } 47 | const posts = await response.json(); 48 | 49 | posts.map((post) => console.log(post.title)); 50 | } catch (error) { 51 | // handle any errors, not sure which one though 🤷‍♀️ 52 | } 53 | ``` 54 | 55 | This is bad because: 56 | 57 | - Error handling happens in multiple places in the function. 58 | - The catch block will catch **any** and **all** errors which makes it difficult to handle different errors differently. 59 | - All the success case code will happen inside of the try block 60 | 61 | ## Multiple Try/Catch Blocks ❌ 62 | 63 | ```ts 64 | let response: Response; 65 | try { 66 | response = await fetch("/posts"); 67 | } catch (error) { 68 | // guard against a network error 69 | return; 70 | } 71 | if (!response.ok) { 72 | // guard against an error response from server 73 | return; 74 | } 75 | 76 | let posts: Post[]; 77 | try { 78 | posts = await response.json(); 79 | } catch (error) { 80 | // guard against an error converting the response to JSON 81 | return; 82 | } 83 | 84 | posts.map((post) => console.log(post.title)); 85 | ``` 86 | 87 | Declaring the variable ahead of time is a little weird and it makes infering the type of the variable a little more difficult. Also, try catch finally blocks can be confusing. 88 | 89 | ## `try` `catch` `finally` can be confusing ❌ 90 | 91 | ```ts 92 | function something() { 93 | try { 94 | throw new Error("something went wrong"); 95 | } catch(error) { 96 | console.log("error happened") 97 | return "error return" 98 | } finally { 99 | console.log("finally happened") 100 | return "finally return" 101 | } 102 | return "something return" 103 | } 104 | console.log(something()) 105 | ``` 106 | 107 | Can every single dev in your team understand what the above code will print out? 108 | -------------------------------------------------------------------------------- /docs/content/home.mdx: -------------------------------------------------------------------------------- 1 | 2 | [![GitHub Repo stars](https://img.shields.io/github/stars/might-fail/ts)](https://github.com/might-fail/ts) 3 | 4 | A TypeScript library for handling async and sync errors without `try` and `catch` blocks. Inspired by other languages that utilize `Result` or `Either` types for safer error handling. 5 | 6 | This works for **sync** and **async** code, and you can choose the error handling style that you like. 7 | 8 | ## Other languages 9 | 10 | * [Swift](https://swift.mightfail.dev/documentation/MightFail) 11 | * [PHP](https://github.com/might-fail/php) 12 | 13 | ## Install 14 | 15 | 16 | 17 | 18 | [![JSR](https://jsr.io/badges/@might/fail)](https://jsr.io/@might/fail) 19 | 20 | 21 | 22 | ```shell 23 | npx jsr add @might/fail 24 | ``` 25 | 26 | 27 | ```shell 28 | yarn dlx jsr add @might/fail 29 | ``` 30 | 31 | 32 | ```shell 33 | pnpm dlx jsr add @might/fail 34 | ``` 35 | 36 | 37 | ```shell 38 | bunx jsr add @might/fail 39 | ``` 40 | 41 | 42 | 43 | ```shell 44 | deno add jsr:@might/fail 45 | ``` 46 | 47 | Or import directly with a jsr specifier 48 | 49 | ```ts 50 | import { mightFail } from "jsr:@might/fail" 51 | ``` 52 | 53 | 54 | 55 | 56 | ```ts 57 | import { mightFail } from "@might/fail" 58 | ``` 59 | 60 | 61 | 62 | 63 | [![npm version](https://img.shields.io/npm/v/might-fail.svg)](https://www.npmjs.com/package/might-fail) 64 | 65 | 66 | ```shell 67 | npm install might-fail 68 | ``` 69 | 70 | 71 | ```shell 72 | yarn add might-fail 73 | ``` 74 | 75 | 76 | ```shell 77 | pnpm add might-fail 78 | ``` 79 | 80 | 81 | ```shell 82 | bun add might-fail 83 | ``` 84 | 85 | 86 | 87 | ```ts 88 | import { mightFail } from "might-fail" 89 | ``` 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | ```ts 99 | import { mightFail } from "might-fail" 100 | const [networkError, result] = await mightFail(fetch("/posts")) 101 | 102 | if (networkError) { 103 | // handle network error 104 | return 105 | } 106 | 107 | if (!result.ok) { 108 | // handle an error response from server 109 | return 110 | } 111 | 112 | const [convertToJSONError, posts] = await mightFail( 113 | result.json() 114 | ) 115 | 116 | if (convertToJSONError) { 117 | // handle convertToJSONError 118 | return 119 | } 120 | 121 | posts.map((post) => console.log(post.title)) 122 | ``` 123 | 124 | 125 | ```ts 126 | import { mightFail } from "might-fail" 127 | const { error: networkError, result } = await mightFail(fetch("/posts")) 128 | 129 | if (networkError) { 130 | // handle network error 131 | return 132 | } 133 | 134 | if (!result.ok) { 135 | // handle an error response from server 136 | return 137 | } 138 | 139 | const { error: convertToJSONError, result: posts } = await mightFail( 140 | result.json() 141 | ) 142 | 143 | if (convertToJSONError) { 144 | // handle convertToJSONError 145 | return 146 | } 147 | 148 | posts.map((post) => console.log(post.title)) 149 | ``` 150 | 151 | 152 | ```ts 153 | import { mightFail } from "might-fail/go" 154 | const [result, networkError] = await mightFail(fetch("/posts")) 155 | 156 | if (networkError) { 157 | // handle network error 158 | return 159 | } 160 | 161 | if (!result.ok) { 162 | // handle an error response from server 163 | return 164 | } 165 | 166 | const [posts, convertToJSONError] = await mightFail( 167 | result.json() 168 | ) 169 | 170 | if (convertToJSONError) { 171 | // handle convertToJSONError 172 | return 173 | } 174 | 175 | posts.map((post) => console.log(post.title)) 176 | ``` 177 | 178 | -------------------------------------------------------------------------------- /docs/content/jsdocs/functions/Fail.mdx: -------------------------------------------------------------------------------- 1 | # Function: Fail() 2 | 3 | ```ts 4 | function Fail(error: unknown): Either 5 | ``` 6 | 7 | A constructor function that takes an error and returns an `Either` object with undefined as the result and the error as the error. 8 | 9 | The error will **always** be an instance of Error. 10 | 11 | ## Type Parameters 12 | 13 | | Type Parameter | Default type | 14 | | ------ | ------ | 15 | | `T` | `any` | 16 | 17 | ## Parameters 18 | 19 | | Parameter | Type | Description | 20 | | ------ | ------ | ------ | 21 | | `error` | `unknown` | | 22 | 23 | ## Returns 24 | 25 | [`Either`](/type-aliases/Either.mdx)\<`T`\> 26 | -------------------------------------------------------------------------------- /docs/content/jsdocs/functions/Might.mdx: -------------------------------------------------------------------------------- 1 | # Function: Might() 2 | 3 | ```ts 4 | function Might(result: T): Either 5 | ``` 6 | 7 | A constructor function that takes a value and returns an `Either` object with the value as the result and undefined as the error. 8 | 9 | ## Type Parameters 10 | 11 | | Type Parameter | 12 | | ------ | 13 | | `T` | 14 | 15 | ## Parameters 16 | 17 | | Parameter | Type | Description | 18 | | ------ | ------ | ------ | 19 | | `result` | `T` | | 20 | 21 | ## Returns 22 | 23 | [`Either`](/type-aliases/Either.mdx)\<`T`\> 24 | -------------------------------------------------------------------------------- /docs/content/jsdocs/functions/mightFail.mdx: -------------------------------------------------------------------------------- 1 | # Function: mightFail() 2 | 3 | ```ts 4 | function mightFail(promise: T): Promise>> 5 | ``` 6 | 7 | Wraps a promise in an Either to safely handle both its resolution and rejection. This function 8 | takes a Promise of type T and returns a Promise which resolves with an object. This object 9 | either contains a 'result' of type T if the promise resolves successfully, or an 'error' of type Error 10 | if the promise is rejected. 11 | 12 | ## Type Parameters 13 | 14 | | Type Parameter | Description | 15 | | ------ | ------ | 16 | | `T` | The type of the result value. | 17 | 18 | ## Parameters 19 | 20 | | Parameter | Type | Description | 21 | | ------ | ------ | ------ | 22 | | `promise` | `T` | The promise to be wrapped in an Either. This is an asynchronous operation that should resolve with a value of type T or reject with an Error. | 23 | 24 | ## Returns 25 | 26 | `Promise`\<[`Either`](/type-aliases/Either.mdx)\<`Awaited`\<`T`\>\>\> 27 | 28 | A Promise that resolves with an Either. This Either is a `Success` with 29 | the 'result' property set to the value resolved by the promise if successful, and 'error' as undefined. 30 | In case of failure, it's a `Failure` with 'result' as undefined and 'error' of type Error. `error` will **always** be an instance of Error. 31 | 32 | ## Example 33 | 34 | ```ts 35 | // Example of wrapping an async function that might fail: 36 | async function fetchData(url: string): Promise { 37 | const response = await fetch(url); 38 | if (!response.ok) { 39 | throw new Error('Network response was not ok'); 40 | } 41 | return response.text(); 42 | } 43 | 44 | const {error, result} = await mightFail(fetchData('https://example.com')); 45 | 46 | if (error) { 47 | console.error('Fetching failed:', error.message); 48 | return; 49 | } 50 | console.log('Fetched data:', result); 51 | ``` 52 | -------------------------------------------------------------------------------- /docs/content/jsdocs/functions/mightFailSync.mdx: -------------------------------------------------------------------------------- 1 | # Function: mightFailSync() 2 | 3 | ```ts 4 | function mightFailSync(func: () => T): Either 5 | ``` 6 | 7 | Wraps a synchronous function in an Either type to safely handle exceptions. This function 8 | executes a provided function that returns a value of type T, capturing any thrown errors. 9 | It returns an object that either contains a 'result' of type T if the function succeeds, 10 | or an 'error' of type Error if the function throws an error. 11 | 12 | ## Type Parameters 13 | 14 | | Type Parameter | Description | 15 | | ------ | ------ | 16 | | `T` | The type of the result value.◊ | 17 | 18 | ## Parameters 19 | 20 | | Parameter | Type | Description | 21 | | ------ | ------ | ------ | 22 | | `func` | () => `T` | A wrapper function that is expected to invoke the throwing function. That function should return a value of type T or throw an error. | 23 | 24 | ## Returns 25 | 26 | [`Either`](/type-aliases/Either.mdx)\<`T`\> 27 | 28 | An object that is either a `Success` with the result property set to the value returned by `func`, 29 | or a `Failure` with the error property set to the caught error. `Success` has a 'result' of type T 30 | and 'error' as null. `Failure` has 'result' as null and 'error' of type Error. 31 | 32 | ## Example 33 | 34 | ```ts 35 | // Example of wrapping a synchronous function that might throw an error: 36 | const {error, result} = mightFailSync(() => JSON.parse("")); 37 | 38 | if (error) { 39 | console.error('Parsing failed:', error); 40 | return; 41 | } 42 | console.log('Parsed object:', result); 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/content/jsdocs/index.mdx: -------------------------------------------------------------------------------- 1 | # might-fail 2 | 3 | ## References 4 | 5 | ### makeMightFail 6 | 7 | Re-exports makeMightFail 8 | 9 | *** 10 | 11 | ### makeMightFailSync 12 | 13 | Re-exports makeMightFailSync 14 | 15 | ## Type Aliases 16 | 17 | - [Either](/type-aliases/Either.mdx) 18 | 19 | ## Variables 20 | 21 | - [default](/variables/default.mdx) 22 | 23 | ## Functions 24 | 25 | - [Fail](/functions/Fail.mdx) 26 | - [Might](/functions/Might.mdx) 27 | - [mightFail](/functions/mightFail.mdx) 28 | - [mightFailSync](/functions/mightFailSync.mdx) 29 | -------------------------------------------------------------------------------- /docs/content/jsdocs/type-aliases/Either.mdx: -------------------------------------------------------------------------------- 1 | # Type Alias: Either\ 2 | 3 | ```ts 4 | type Either: { 5 | error: Error; 6 | result: undefined; 7 | } & [Error, undefined] | { 8 | error: undefined; 9 | result: T; 10 | } & [undefined, T]; 11 | ``` 12 | 13 | Either type represents a data structure that encapsulates a successful result or an Error. 14 | It wraps the result of a Promise in an object, making it easier to handle errors by returning 15 | an object that either contains a 'result' value of type T (if successful), or an 'error' of type Error. 16 | 17 | ## Type Parameters 18 | 19 | | Type Parameter | Description | 20 | | ------ | ------ | 21 | | `T` | The type of the result value. | 22 | -------------------------------------------------------------------------------- /docs/content/jsdocs/variables/default.mdx: -------------------------------------------------------------------------------- 1 | # Variable: default 2 | 3 | ```ts 4 | const default: { 5 | Fail: (error: unknown) => Either; 6 | makeMightFail: (func: T) => (...funcArgs: Parameters) => Promise>>>; 7 | makeMightFailSync: (func: T) => (...funcArgs: Parameters) => Either>; 8 | Might: (result: T) => Either; 9 | mightFail: MightFail<"standard">; 10 | mightFailSync: (func: () => T) => Either; 11 | }; 12 | ``` 13 | 14 | ## Type declaration 15 | 16 | | Name | Type | 17 | | ------ | ------ | 18 | | `Fail` | \<`T`\>(`error`: `unknown`) => [`Either`](/type-aliases/Either.mdx)\<`T`\> | 19 | | `makeMightFail` | \<`T`\>(`func`: `T`) => (...`funcArgs`: `Parameters`\<`T`\>) => `Promise`\<[`Either`](/type-aliases/Either.mdx)\<`UnwrapPromise`\<`ReturnType`\<`T`\>\>\>\> | 20 | | `makeMightFailSync` | \<`T`\>(`func`: `T`) => (...`funcArgs`: `Parameters`\<`T`\>) => [`Either`](/type-aliases/Either.mdx)\<`ReturnType`\<`T`\>\> | 21 | | `Might` | \<`T`\>(`result`: `T`) => [`Either`](/type-aliases/Either.mdx)\<`T`\> | 22 | | `mightFail` | `MightFail`\<`"standard"`\> | 23 | | `mightFailSync` | \<`T`\>(`func`: () => `T`) => [`Either`](/type-aliases/Either.mdx)\<`T`\> | 24 | -------------------------------------------------------------------------------- /docs/content/more-things/might-and-fail.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Might & Fail 4 | 5 | Two utility functions that construct an Either and promotes working with the Either type directly. 6 | 7 | ## When to use them 8 | 9 | 10 | The opinion of this library is that throwing Errors is a good thing. `throw` is good, but `try`, `catch`, `finally` blocks are bad. 11 | 12 | 13 | `mightFail` and `mightFailSync` are alternatives to `try`, `catch`, `finally` blocks. These functions catch the errors and suply a much better interface for working with errors. 14 | 15 | However, there may be times when you don't want to throw Errors in your code. I can imagine [`mjs`](https://github.com/cesanta/mjs) or [`quickjs`](https://bellard.org/quickjs/) might be good use cases for this. 16 | 17 | Or maybe you don't want to catch all errors that might be thrown because some of them need to propagate to the top of the call stack for framework reasons. 18 | 19 | So this is for when you don't want to throw errors, but you want to have functions that return an Either type. 20 | 21 | 22 | ## Syncronous Either 23 | 24 | ```ts 25 | import { Either, Might, Fail } from 'might-fail' 26 | 27 | export function foo(): Either<{message: string}> { 28 | // ... 29 | 30 | if (condition) { 31 | // notice that we don't have to wrap the string to an Error construct 32 | // because Fail will automatically wraps it in Error for us. 33 | return Fail("Something went wrong") 34 | } 35 | 36 | if (condition2) { 37 | return Fail("Something else went wrong") 38 | } 39 | 40 | return Might({message: "All good"}) 41 | } 42 | ``` 43 | 44 | 45 | You're not allowed to pass `undefined` to the `Might` function. 46 | 47 | ```ts 48 | // Either should not have both sides as `undefined` 49 | // export type Either = [Error, undefined] | [undefined, T] 50 | 51 | import { Either, Might } from 'might-fail/tuple' 52 | 53 | const undefinedAsResult = Might(undefined) // ❌ This is not OK. TypeScript won't allow it. 54 | 55 | const explicitlyEmpty = Might(null) // ✅ This is fine because it doesn't go against the rule of the Either concept 56 | ``` 57 | 58 | 59 | 60 | ## Asyncronous Either 61 | 62 | ```ts 63 | import { Either, Might, Fail } from 'might-fail' 64 | 65 | export async function foo(): Promise> { 66 | // ... 67 | 68 | if (condition) { 69 | // notice that we don't have to wrap the string to an Error construct 70 | // because Fail will automatically wraps it in Error for us. 71 | return Fail("Something went wrong") 72 | } 73 | 74 | if (condition2) { 75 | return Fail("Something else went wrong") 76 | } 77 | 78 | return Might({message: "All good"}) 79 | } 80 | ``` 81 | 82 | -------------------------------------------------------------------------------- /docs/content/more-things/static-methods.mdx: -------------------------------------------------------------------------------- 1 | # Static Methods 2 | 3 | `mightFail` has static methods that wrap the corresponding static methods of `Promise`. 4 | 5 | - [`Promise.race`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race) 6 | - [`Promise.all`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all) 7 | - [`Promise.any`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any) 8 | 9 | 10 | - `await mightFail.all([])` 11 | - `await mightFail.race([])` 12 | - `await mightFail.any([])` 13 | 14 | These are identical to the static methods on `Promise` but they return an [`Either`](/either) type. 15 | 16 | ## Example 17 | 18 | ```ts 19 | import { mightFail } from 'might-fail' 20 | 21 | const [error, result] = await mightFail.all([ 22 | Promise.resolve({ message: "success" }), 23 | Promise.resolve({ message: "success2" }), 24 | ]); 25 | 26 | if (error) { 27 | // error from Promise.all 28 | return 29 | } 30 | 31 | // result is an array of the results of the promises 32 | console.log(result.map((r) => r.message)) 33 | ``` 34 | -------------------------------------------------------------------------------- /examples/makeMightFailBasic.ts: -------------------------------------------------------------------------------- 1 | import { makeMightFail, makeMightFailSync } from "../src/makeMightFail" 2 | 3 | const resolve = (value: { message: string }) => Promise.resolve(value) 4 | async function asyncMain() { 5 | const func = makeMightFail(resolve) 6 | const { error, result } = await func({ message: "success" }) 7 | if (error) { 8 | console.error(error) 9 | return 10 | } 11 | console.log(result.message) 12 | } 13 | 14 | asyncMain() 15 | 16 | 17 | function syncMain() { 18 | const parseJSON = makeMightFailSync(JSON.parse) 19 | 20 | const [error, result] = parseJSON("{invalid}") 21 | if (error) { 22 | console.error(error) 23 | return 24 | } 25 | console.log(result.message) 26 | } 27 | 28 | syncMain() -------------------------------------------------------------------------------- /examples/mightAndFail.ts: -------------------------------------------------------------------------------- 1 | import { mightFail, Fail, type Either, makeMightFail, Might } from "../src" 2 | 3 | export async function someFunction( 4 | promise1: Promise = Promise.resolve("success"), 5 | promise2: Promise = Promise.resolve(1) 6 | ): Promise> { 7 | const [error1, result1] = await mightFail(promise1) 8 | if (error1) { 9 | return Fail(error1) 10 | } 11 | if (result1 === null) { 12 | return Fail(new Error("Could not get the the thing.")) 13 | } 14 | 15 | return Might({ stuff: true }) 16 | 17 | // const [error2] = await mightFail(promise2) 18 | // if (error2) { 19 | // return [error2, undefined] 20 | // } 21 | 22 | // return [undefined, { stuff: true }] 23 | } 24 | 25 | // async function _someFunction( 26 | // promise1: Promise = Promise.resolve("success"), 27 | // promise2: Promise = Promise.resolve(1) 28 | // ) { 29 | // const [error1, result1] = await mightFail(promise1) 30 | // if (error1) { 31 | // throw error1 32 | // } 33 | // if (result1 === null) { 34 | // throw new Error("Could not get the the thing.") 35 | // } 36 | 37 | // const [error2] = await mightFail(promise2) 38 | // if (error2) { 39 | // throw error2 40 | // } 41 | 42 | // return { stuff: true } 43 | // } 44 | 45 | // export const someFunction = makeMightFail(_someFunction) 46 | 47 | 48 | async function something(): Promise> { 49 | const [error, result] = await mightFail(Promise.resolve("success")) 50 | if (error) { 51 | return Fail(error) 52 | } 53 | return Might(result) 54 | } 55 | -------------------------------------------------------------------------------- /examples/mightFailBasic.ts: -------------------------------------------------------------------------------- 1 | import { mightFail } from "../src" 2 | 3 | async function main() { 4 | const { error, result } = await mightFail(Promise.resolve({ message: "success" })) 5 | if (error) { 6 | console.error(error) 7 | return 8 | } 9 | console.log(result.message) 10 | } 11 | 12 | main() 13 | 14 | function someResponse() { 15 | const responses = [ 16 | { status: 200, data: { id: 1, name: "John Doe" }, json: async () => ({ status: 200, data: { id: 1, name: "John Doe" } }) }, 17 | { status: 404, error: "Not Found", json: async () => ({ status: 404, error: "Not Found" }) }, 18 | { status: 500, error: "Internal Server Error", json: async () => ({ status: 500, error: "Internal Server Error" }) }, 19 | { status: 201, data: { id: 2, name: "Jane Smith", created: true }, json: async () => ({ status: 201, data: { id: 2, name: "Jane Smith", created: true } }) }, 20 | { status: 400, error: "Bad Request", details: ["Invalid email", "Password too short"], json: async () => ({ status: 400, error: "Bad Request", details: ["Invalid email", "Password too short"] }) }, 21 | ]; 22 | 23 | const randomIndex = Math.floor(Math.random() * responses.length); 24 | const randomResponse = responses[randomIndex]; 25 | 26 | return randomResponse; 27 | } 28 | 29 | async function main2() { 30 | const res = someResponse() 31 | const [error, result] = await mightFail(res.json()) 32 | if (error) { 33 | console.error(error) 34 | return 35 | } 36 | console.log(result) 37 | } 38 | 39 | main2() 40 | 41 | -------------------------------------------------------------------------------- /examples/mightFailBasicGo.ts: -------------------------------------------------------------------------------- 1 | import { mightFail } from "../src/go" 2 | 3 | async function main() { 4 | const [error, result] = await mightFail(Promise.resolve({ message: "success" })) 5 | if (error) { 6 | console.error(error) 7 | return 8 | } 9 | console.log(result.message) 10 | } 11 | 12 | main() 13 | -------------------------------------------------------------------------------- /examples/mightFailBasicTuple.ts: -------------------------------------------------------------------------------- 1 | import { mightFail } from "../src" 2 | 3 | async function success() { 4 | const [error, result] = await mightFail(Promise.resolve({ message: "success" })) 5 | if (error) { 6 | console.error(error) 7 | return 8 | } 9 | console.log(result.message) 10 | } 11 | 12 | success() 13 | 14 | async function error() { 15 | const [error, result] = await mightFail(new Promise<{ message: string }>((res, rej) => rej("error"))) 16 | if (error) { 17 | console.error(error) 18 | return 19 | } 20 | console.log(result.message) 21 | } 22 | 23 | error() 24 | 25 | -------------------------------------------------------------------------------- /examples/mightFailStaticMethods.ts: -------------------------------------------------------------------------------- 1 | import { mightFail } from "../src" 2 | import { mightFail as mightFailGo } from "../src/go" 3 | 4 | async function all() { 5 | const { error, result } = await mightFail.all([ 6 | Promise.resolve({ message: "success" }), 7 | Promise.resolve({ message: "success2" }) 8 | ]) 9 | if (error) { 10 | console.error(error) 11 | return 12 | } 13 | console.log(result.map((r) => r.message)) 14 | } 15 | 16 | all() 17 | 18 | async function allDifferentTypes() { 19 | const { error, result } = await mightFail.all([Promise.resolve({ message: "success" }), Promise.resolve(5)]) 20 | if (error) { 21 | console.error(error) 22 | return 23 | } 24 | const [res1, res2] = result 25 | console.log(res1, res2) 26 | } 27 | 28 | allDifferentTypes() 29 | 30 | async function allDestructured() { 31 | const { error, result: [result1, result2] = [] } = await mightFail.all([ 32 | Promise.resolve({ message: "success" }), 33 | Promise.resolve(5) 34 | ]) 35 | if (error) { 36 | console.error(error) 37 | return 38 | } 39 | console.log(result1, result2) 40 | } 41 | 42 | allDestructured() 43 | 44 | async function allTuple() { 45 | const [error, result] = await mightFail.all([ 46 | Promise.resolve({ message: "success" }), 47 | Promise.resolve({ message: "success2" }) 48 | ]) 49 | if (error) { 50 | console.error(error) 51 | return 52 | } 53 | console.log(result.map((r) => r.message)) 54 | } 55 | 56 | allTuple() 57 | 58 | async function allTupleDestructured() { 59 | const [error, [result1, result2] = []] = await mightFail.all([ 60 | Promise.resolve({ message: "success" }), 61 | Promise.resolve({ message: "success2" }) 62 | ]) 63 | if (error) { 64 | console.error(error) 65 | return 66 | } 67 | console.log(result1, result2) 68 | } 69 | 70 | allTupleDestructured() 71 | 72 | async function allGo() { 73 | const [result, error] = await mightFailGo.all([ 74 | Promise.resolve({ message: "success" }), 75 | Promise.resolve({ message: "success2" }) 76 | ]) 77 | if (error) { 78 | console.error(error) 79 | return 80 | } 81 | console.log(result.map((r) => r.message)) 82 | } 83 | 84 | allGo() 85 | 86 | async function returnStringAfter(message: string, time: number) { 87 | await new Promise((resolve) => setTimeout(resolve, time)) 88 | return { message } 89 | } 90 | 91 | async function race() { 92 | const { error, result } = await mightFail.race([returnStringAfter("fast", 100), returnStringAfter("slow", 200)]) 93 | if (error) { 94 | console.error(error) 95 | return 96 | } 97 | console.log(result.message) 98 | } 99 | 100 | race() 101 | 102 | async function any() { 103 | const promises = [ 104 | Promise.reject(new Error("Failure 1")), 105 | returnStringAfter("success", 100), 106 | Promise.reject(new Error("Failure 2")) 107 | ] 108 | 109 | const { error, result } = await mightFail.any(promises) 110 | if (error) { 111 | console.error(error) 112 | return 113 | } 114 | console.log(result.message) 115 | } 116 | 117 | any() 118 | 119 | async function raceTuple() { 120 | const [error, result] = await mightFail.race([returnStringAfter("fast", 100), returnStringAfter("slow", 200)]) 121 | if (error) { 122 | console.error(error) 123 | return 124 | } 125 | console.log(result.message) 126 | } 127 | 128 | raceTuple() 129 | 130 | async function anyTuple() { 131 | const [error, result] = await mightFail.any([ 132 | Promise.reject(new Error("Failure 1")), 133 | returnStringAfter("success", 100), 134 | Promise.reject(new Error("Failure 2")) 135 | ]) 136 | if (error) { 137 | console.error(error) 138 | return 139 | } 140 | console.log(result.message) 141 | } 142 | 143 | anyTuple() 144 | 145 | async function raceGo() { 146 | const [result, error] = await mightFailGo.race([returnStringAfter("fast", 100), returnStringAfter("slow", 200)]) 147 | if (error) { 148 | console.error(error) 149 | return 150 | } 151 | console.log(result.message) 152 | } 153 | 154 | raceGo() 155 | 156 | async function anyGo() { 157 | const [result, error] = await mightFailGo.any([ 158 | Promise.reject(new Error("Failure 1")), 159 | returnStringAfter("success", 100), 160 | Promise.reject(new Error("Failure 2")) 161 | ]) 162 | if (error) { 163 | console.error(error) 164 | return 165 | } 166 | console.log(result.message) 167 | } 168 | 169 | anyGo() 170 | 171 | // Call all the new functions 172 | race() 173 | any() 174 | raceTuple() 175 | anyTuple() 176 | raceGo() 177 | anyGo() 178 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["./**/*"], 4 | "compilerOptions": { 5 | "rootDir": "..", 6 | "baseUrl": ".." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /jsr.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@might/fail", 3 | "version": "0.7.4", 4 | "exports": { 5 | ".": "./src/index.ts", 6 | "./go": "./src/go/index.ts" 7 | }, 8 | "publish": { 9 | "include": [ 10 | "LICENSE", 11 | "README.md", 12 | "src/**/*.ts" 13 | ] 14 | }, 15 | "license": "MIT" 16 | } 17 | -------------------------------------------------------------------------------- /package.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "might-fail", 3 | "version": "0.7.4", 4 | "description": "A better way to handle errors in JavaScript and TypeScript. Handle async and sync errors without `try` and `catch` blocks.", 5 | "main": "dist/cjs/index.cjs", 6 | "module": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "type": "module", 9 | "exports": { 10 | ".": { 11 | "import": "./dist/index.js", 12 | "require": "./dist/cjs/index.cjs", 13 | "types": "./dist/index.d.ts" 14 | }, 15 | "./go": { 16 | "import": "./dist/go/index.js", 17 | "require": "./dist/cjs/go/index.cjs", 18 | "types": "./dist/go/index.d.ts" 19 | } 20 | }, 21 | "scripts": { 22 | "build": "bun build.ts", 23 | "test": "vitest", 24 | "typedoc": "typedoc src", 25 | "prepublishOnly": "npm run build" 26 | }, 27 | "files": [ 28 | "dist/**/*.js", 29 | "dist/**/*.cjs", 30 | "dist/**/*.d.ts", 31 | "dist/**/*.ts" 32 | ], 33 | "author": "Sam Meech-Ward (https://smw.wtf)", 34 | "license": "MIT", 35 | "repository": { 36 | "type": "git", 37 | "url": "git+https://github.com/might-fail/ts.git" 38 | }, 39 | "keywords": [ 40 | "mightfail", 41 | "might fail", 42 | "either", 43 | "error", 44 | "exception", 45 | "fail", 46 | "failure", 47 | "maybe", 48 | "result", 49 | "success", 50 | "try", 51 | "typescript" 52 | ], 53 | "devDependencies": { 54 | "@types/glob": "^8.0.0", 55 | "@types/node": "^22.7.5", 56 | "esbuild": "^0.25.0", 57 | "glob": "7.2.3", 58 | "jsdoc-to-markdown": "^9.0.2", 59 | "typedoc": "^0.27.9", 60 | "typedoc-plugin-markdown": "^4.2.9", 61 | "typescript": "^5.6.2", 62 | "vitest": "^3.0.7" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Either.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Either type represents a data structure that encapsulates a successful result or an Error. 3 | * It wraps the result of a Promise in an object, making it easier to handle errors by returning 4 | * an object that either contains a 'result' value of type T (if successful), or an 'error' of type Error. 5 | * 6 | * @template T The type of the result value. 7 | * @template E extends Error - The error type that is scoped to any type extending type Error. 8 | */ 9 | export type Either = 10 | | ({ 11 | error: E 12 | result: undefined 13 | } & [E, undefined]) 14 | | ({ 15 | result: T 16 | error: undefined 17 | } & [undefined, T]) 18 | 19 | // Note this array interface is undesireable, we would much rather use just an itterator here but TS doesn't yet support that well enough 20 | // So it has an array interface and proxies array methods. 21 | // See the utils `makeEither` for more information. 22 | -------------------------------------------------------------------------------- /src/go/Either.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Either type represents a data structure that encapsulates a successful result or an Error. 3 | * It wraps the result of a Promise in an object, making it easier to handle errors by returning 4 | * an object that either contains a 'result' value of type T (if successful), or an 'error' of type Error. 5 | * 6 | * @template T The type of the result value. 7 | * @template E extends Error - The error type that is scoped to any type extending type Error. 8 | */ 9 | export type Either = 10 | | ({ 11 | error: E 12 | result: undefined 13 | } & [undefined, E]) 14 | | ({ 15 | error: undefined 16 | result: T 17 | } & [T, undefined]) 18 | 19 | // Note this array interface is undesireable, we would much rather use just an itterator here but TS doesn't yet support that well enough 20 | // So it has an array interface and proxies array methods. 21 | // See the utils `makeEither` for more information. 22 | -------------------------------------------------------------------------------- /src/go/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * 4 | * This module contains the interface to use the result of mightFail as an error-last tuple. 5 | * 6 | * This aligns with the proposal-safe-assignment-operator: https://github.com/arthurfiorette/proposal-safe-assignment-operator?tab=readme-ov-file#why-not-data-first 7 | * 8 | * If you want to use error-last style, just like golang, use the `/go` module. 9 | * 10 | + * @example 11 | + * ```ts 12 | + * import { mightFail } from "@might/fail/tuple"; 13 | + * 14 | + * const [error, result] = await mightFail(promise); 15 | + * ``` 16 | */ 17 | 18 | import { type Either } from "./Either" 19 | import { mightFail, mightFailSync, Might, Fail } from "./mightFail" 20 | import { makeMightFail, makeMightFailSync } from "./makeMightFail" 21 | 22 | export { Either, mightFail, makeMightFail, mightFailSync, makeMightFailSync, Might, Fail } 23 | const defaultExport = { 24 | mightFail, 25 | makeMightFail, 26 | mightFailSync, 27 | makeMightFailSync, 28 | Might, 29 | Fail 30 | } 31 | export default defaultExport 32 | -------------------------------------------------------------------------------- /src/go/makeMightFail.ts: -------------------------------------------------------------------------------- 1 | import { type Either } from "./Either" 2 | import { mightFail, mightFailSync } from "./mightFail" 3 | 4 | /** 5 | * Utility type that unwraps a Promise type. If T is a Promise, it extracts the type the Promise resolves to, 6 | * providing direct access to the underlying value type. 7 | * 8 | * @template T The type to be unwrapped if it's a Promise. 9 | */ 10 | type UnwrapPromise = T extends Promise ? U : T 11 | 12 | /** 13 | * Wraps a promise-returning function in another function that instead of returning a Promise directly, 14 | * returns a Promise that resolves with an Either. This allows for the handling of both resolved values and 15 | * errors in a consistent, functional way. 16 | * 17 | 18 | * @template T The function type that returns a Promise. 19 | * @param {T} func - The async function to be wrapped. This function should return a Promise. 20 | * @return {Function} A new function that, when called, returns a Promise that resolves with an Either tuple. 21 | * The Either tuple contains either the resolved value of the original Promise as the first element and undefined as the second, 22 | * or undefined as the first element and an Error as the second if the Promise was rejected. 23 | * 24 | * @example 25 | * // Example of wrapping an async function that might fail: 26 | * async function fetchData(url: string): Promise { 27 | * const response = await fetch(url); 28 | * if (!response.ok) { 29 | * throw new Error('Network response was not ok'); 30 | * } 31 | * return response.text(); 32 | * } 33 | * 34 | * const safeFetchData = makeMightFail(fetchData); 35 | * const [result, error] = await safeFetchData('https://example.com'); 36 | * 37 | * if (error) { 38 | * console.error('Fetching failed:', error.message); 39 | * return 40 | * } 41 | * console.log('Fetched data:', result); 42 | */ 43 | export function makeMightFail Promise, E extends Error = Error>( 44 | func: T 45 | ): (...funcArgs: Parameters) => Promise>, E>> { 46 | return async (...args: Parameters) => { 47 | const promise = func(...args) 48 | return mightFail(promise) 49 | } 50 | } 51 | 52 | /** 53 | * Wraps a synchronous function that might throw an exception in another function that, 54 | * instead of throwing, returns an Either tuple. This tuple contains either the value returned by the function 55 | * if it executes successfully as the first element and undefined as the second, or undefined as the first element 56 | * and an Error as the second if the function throws. 57 | * 58 | 59 | * @template T The function type that might throw an error. 60 | * @param {T} func - The function to be wrapped. This function might throw an exception. 61 | * @return {Function} A new function that, when called, returns an Either tuple with either a result or an error. 62 | * 63 | * @example 64 | * // Example of wrapping a synchronous function that might throw an error: 65 | * function parseJSON(jsonString: string) { 66 | * return JSON.parse(jsonString); // This might throw 67 | * } 68 | * 69 | * const safeParseJSON = makeMightFailSync(parseJSON); 70 | * const [result, error] = safeParseJSON('{"valid": "json"}'); 71 | * 72 | * if (error) { 73 | * console.error('Parsing failed:', error); 74 | * return; 75 | * } 76 | * console.log('Parsed object:', result); 77 | */ 78 | export function makeMightFailSync any, E extends Error = Error>( 79 | func: T 80 | ): (...funcArgs: Parameters) => Either, E> { 81 | return (...args: Parameters) => { 82 | const throwingFunction = () => func(...args) 83 | return mightFailSync(throwingFunction) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/go/mightFail.ts: -------------------------------------------------------------------------------- 1 | import standard from "../index" 2 | import { type Either } from "./Either" 3 | import { createEither } from "../utils/createEither" 4 | import { makeProxyHandler } from "../utils/staticMethodsProxy" 5 | import { MightFail, MightFailFunction } from "../utils/utils.type" 6 | import { mightFailFunction as standardMightFailFunction } from "../utils/mightFailFunction" 7 | 8 | const mightFailFunction: MightFailFunction<"go"> = async function (promise: T) { 9 | const { result, error } = await standardMightFailFunction(promise) 10 | return error 11 | ? createEither, E, "go">({ result: undefined, error: error as E }, "go") 12 | : createEither, E, "go">({ result, error: undefined }, "go") 13 | } 14 | 15 | /** 16 | * Wraps a promise in an Either to safely handle both its resolution and rejection. This function 17 | * takes a Promise of type T and returns a Promise which resolves with an Either tuple. This tuple 18 | * either contains the resolved value of type T as the first element and undefined as the second if the promise 19 | * resolves successfully, or undefined as the first element and an Error as the second if the promise is rejected. 20 | * 21 | 22 | * @template T The type of the result value. 23 | * @param {Promise} promise - The promise to be wrapped in an Either. This is an asynchronous operation that 24 | * should resolve with a value of type T or reject with an Error. 25 | * @return {Promise>} A Promise that resolves with an Either tuple. This Either is [T, undefined] with 26 | * the first element set to the value resolved by the promise if successful, and [undefined, Error] in case of failure. 27 | * The error will always be an instance of Error. 28 | * 29 | * @example 30 | * // Example of wrapping an async function that might fail: 31 | * async function fetchData(url: string): Promise { 32 | * const response = await fetch(url); 33 | * if (!response.ok) { 34 | * throw new Error('Network response was not ok'); 35 | * } 36 | * return response.text(); 37 | * } 38 | * 39 | * const [result, error] = await mightFail(fetchData('https://example.com')); 40 | * 41 | * if (error) { 42 | * console.error('Fetching failed:', error.message); 43 | * return; 44 | * } 45 | * console.log('Fetched data:', result); 46 | */ 47 | export const mightFail: MightFail<"go"> = new Proxy( 48 | mightFailFunction, 49 | makeProxyHandler(mightFailFunction) 50 | ) as MightFail<"go"> 51 | 52 | /** 53 | * Wraps a synchronous function in an Either type to safely handle exceptions. This function 54 | * executes a provided function that returns a value of type T, capturing any thrown errors. 55 | * It returns an Either tuple that either contains the value of type T as the first element and undefined as the second 56 | * if the function succeeds, or undefined as the first element and an Error as the second if the function throws an error. 57 | * 58 | 59 | * @template T The type of the result value. 60 | * @param {() => T} func - A wrapper function that is expected to invoke the throwing function. 61 | * That function should return a value of type T or throw an error. 62 | * @return {Either} An Either tuple that is either [T, undefined] with the first element set to the value returned by `func`, 63 | * or [undefined, Error] with the second element set to the caught error. 64 | * @example 65 | * // Example of wrapping a synchronous function that might throw an error: 66 | * const [result, error] = mightFailSync(() => JSON.parse("")); 67 | * 68 | * if (error) { 69 | * console.error('Parsing failed:', error); 70 | * return; 71 | * } 72 | * console.log('Parsed object:', result); 73 | */ 74 | export function mightFailSync(func: () => T): Either { 75 | const { result, error } = standard.mightFailSync(func) 76 | return error 77 | ? createEither({ result: undefined, error: error as E }, "go") 78 | : createEither({ result, error: undefined }, "go") 79 | } 80 | 81 | /** 82 | * A constructor function that takes a value and returns an Either object with the value as the result and undefined as the error. 83 | * 84 | * @param result 85 | * @constructor 86 | */ 87 | export function Might(result: T): Either { 88 | const standardMight = standard.Might(result) 89 | return createEither({ result: standardMight.result as T, error: undefined }, "go") 90 | } 91 | 92 | /** 93 | * A constructor function that takes an error and returns an Either object with undefined as the result and the error as the error. 94 | * 95 | * The error will **always** be an instance of Error. 96 | * 97 | * @param error 98 | * @constructor 99 | */ 100 | export function Fail(error: unknown): Either { 101 | const standardFail = standard.Fail(error) 102 | return createEither({ result: undefined, error: standardFail.error! as E }, "go") 103 | } 104 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { type Either } from "./Either" 2 | import { mightFail, mightFailSync, Might, Fail } from "./mightFail" 3 | import { makeMightFail, makeMightFailSync } from "./makeMightFail" 4 | 5 | export { Either, mightFail, makeMightFail, mightFailSync, makeMightFailSync, Might, Fail } 6 | const defaultExport = { 7 | mightFail, 8 | makeMightFail, 9 | mightFailSync, 10 | makeMightFailSync, 11 | Might, 12 | Fail 13 | } 14 | export default defaultExport 15 | -------------------------------------------------------------------------------- /src/makeMightFail.ts: -------------------------------------------------------------------------------- 1 | import { type Either } from "./Either" 2 | import { mightFail, mightFailSync } from "./mightFail" 3 | 4 | /** 5 | * Utility type that unwraps a Promise type. If T is a Promise, it extracts the type the Promise resolves to, 6 | * providing direct access to the underlying value type. 7 | * 8 | * @template T The type to be unwrapped if it's a Promise. 9 | */ 10 | type UnwrapPromise = T extends Promise ? U : T 11 | 12 | /** 13 | * Wraps a promise-returning function in another function that instead of returning a Promise directly, 14 | * returns a Promise that resolves with an Either. This allows for the handling of both resolved values and 15 | * errors in a consistent, functional way. 16 | * 17 | 18 | * @template T The function type that returns a Promise. 19 | * @param {T} func - The async function to be wrapped. This function should return a Promise. 20 | * @return {Function} A new function that, when called, returns a Promise that resolves with an Either object. 21 | * The Either object contains either a 'result' with the resolved value of the original Promise, or an 'error' if the Promise was rejected. 22 | * 23 | * @example 24 | * // Example of wrapping an async function that might fail: 25 | * async function fetchData(url: string): Promise { 26 | * const response = await fetch(url); 27 | * if (!response.ok) { 28 | * throw new Error('Network response was not ok'); 29 | * } 30 | * return response.text(); 31 | * } 32 | * 33 | * const safeFetchData = makeMightFail(fetchData); 34 | * const {error, result} = await safeFetchData('https://example.com'); 35 | * 36 | * if (error) { 37 | * console.error('Fetching failed:', error.message); 38 | * return 39 | * } 40 | * console.log('Fetched data:', result); 41 | */ 42 | export function makeMightFail Promise, E extends Error = Error>( 43 | func: T 44 | ): (...funcArgs: Parameters) => Promise>, E>> { 45 | return async (...args: Parameters) => { 46 | const promise = func(...args) 47 | return mightFail(promise) 48 | } 49 | } 50 | 51 | /** 52 | * Wraps a synchronous function that might throw an exception in another function that, 53 | * instead of throwing, returns an Either object. This object contains either a 'result' 54 | * with the value returned by the function if it executes successfully, or an 'error' if the function throws. 55 | * 56 | 57 | * @template T The function type that might throw an error. 58 | * @param {T} func - The function to be wrapped. This function might throw an exception. 59 | * @return {Function} A new function that, when called, returns an Either object with either a 'result' or an 'error'. 60 | * 61 | * @example 62 | * // Example of wrapping a synchronous function that might throw an error: 63 | * function parseJSON(jsonString: string) { 64 | * return JSON.parse(jsonString); // This might throw 65 | * } 66 | * 67 | * const safeParseJSON = makeMightFailSync(parseJSON); 68 | * const {error, result} = safeParseJSON('{"valid": "json"}'); 69 | * 70 | * if (error) { 71 | * console.error('Parsing failed:', error); 72 | * return; 73 | * } 74 | * console.log('Parsed object:', result); 75 | */ 76 | export function makeMightFailSync any, E extends Error = Error>( 77 | func: T 78 | ): (...funcArgs: Parameters) => Either, E> { 79 | return (...args: Parameters) => { 80 | const throwingFunction = () => func(...args) 81 | return mightFailSync(throwingFunction) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/mightFail.ts: -------------------------------------------------------------------------------- 1 | import { type Either } from "./Either" 2 | import { makeProxyHandler } from "./utils/staticMethodsProxy" 3 | import { handleError } from "./utils/errors" 4 | import { createEither } from "./utils/createEither" 5 | import { MightFail } from "./utils/utils.type" 6 | import { mightFailFunction } from "./utils/mightFailFunction" 7 | 8 | /** 9 | * Wraps a promise in an Either to safely handle both its resolution and rejection. This function 10 | * takes a Promise of type T and returns a Promise which resolves with an object. This object 11 | * either contains a 'result' of type T if the promise resolves successfully, or an 'error' of type Error 12 | * if the promise is rejected. 13 | * 14 | * @template T The type of the result value. 15 | * @param {Promise} promise - The promise to be wrapped in an Either. This is an asynchronous operation that 16 | * should resolve with a value of type T or reject with an Error. 17 | * @return {Promise>} A Promise that resolves with an Either. This Either is a `Success` with 18 | * the 'result' property set to the value resolved by the promise if successful, and 'error' as undefined. 19 | * In case of failure, it's a `Failure` with 'result' as undefined and 'error' of type Error. `error` will **always** be an instance of Error. 20 | * 21 | * @example 22 | * // Example of wrapping an async function that might fail: 23 | * async function fetchData(url: string): Promise { 24 | * const response = await fetch(url); 25 | * if (!response.ok) { 26 | * throw new Error('Network response was not ok'); 27 | * } 28 | * return response.text(); 29 | * } 30 | * 31 | * const {error, result} = await mightFail(fetchData('https://example.com')); 32 | * 33 | * if (error) { 34 | * console.error('Fetching failed:', error.message); 35 | * return; 36 | * } 37 | * console.log('Fetched data:', result); 38 | */ 39 | export const mightFail: MightFail<"standard"> = new Proxy( 40 | mightFailFunction, 41 | makeProxyHandler(mightFailFunction) 42 | ) as MightFail<"standard"> 43 | 44 | /** 45 | * Wraps a synchronous function in an Either type to safely handle exceptions. This function 46 | * executes a provided function that returns a value of type T, capturing any thrown errors. 47 | * It returns an object that either contains a 'result' of type T if the function succeeds, 48 | * or an 'error' of type Error if the function throws an error. 49 | * 50 | * @template T The type of the result value.◊ 51 | * @param {() => T} func - A wrapper function that is expected to invoke the throwing function. 52 | * That function should return a value of type T or throw an error. 53 | * @return {Either} An object that is either a `Success` with the result property set to the value returned by `func`, 54 | * or a `Failure` with the error property set to the caught error. `Success` has a 'result' of type T 55 | * and 'error' as null. `Failure` has 'result' as null and 'error' of type Error. 56 | * @example 57 | * // Example of wrapping a synchronous function that might throw an error: 58 | * const {error, result} = mightFailSync(() => JSON.parse("")); 59 | * 60 | * if (error) { 61 | * console.error('Parsing failed:', error); 62 | * return; 63 | * } 64 | * console.log('Parsed object:', result); 65 | */ 66 | export const mightFailSync = function mightFailSync(func: () => T): Either { 67 | try { 68 | const result = func() 69 | return createEither({ error: undefined, result }) 70 | } catch (err) { 71 | const error = handleError(err) 72 | return createEither({ error, result: undefined }) 73 | } 74 | } 75 | 76 | /** 77 | * A constructor function that takes a value and returns an `Either` object with the value as the result and undefined as the error. 78 | * 79 | * @param result 80 | */ 81 | export const Might = function Might(result: T): Either { 82 | return createEither({ result, error: undefined }) 83 | } 84 | 85 | /** 86 | * A constructor function that takes an error and returns an `Either` object with undefined as the result and the error as the error. 87 | * 88 | * The error will **always** be an instance of Error. 89 | * 90 | * @param error 91 | */ 92 | export const Fail = function Fail(error: unknown): Either { 93 | return createEither({ result: undefined, error: handleError(error) }) 94 | } 95 | -------------------------------------------------------------------------------- /src/utils/createEither.ts: -------------------------------------------------------------------------------- 1 | import { AnyEither, EitherMode } from "./utils.type" 2 | import type { Either as StandardEither } from "../Either" 3 | import type { Either as GoEither } from "../go/Either" 4 | 5 | // This is not how we intended the tuple feature to work but this is the only way we could currently get TypeScript to play nice 6 | // this really should just be an interator on the either object, but it's much more complicated because of TS. 7 | // All the details are in this PR https://github.com/might-fail/ts/pull/7#issuecomment-2395122593 8 | // hopefully we can change this with a future version of TS. 9 | 10 | export const createEither = ( 11 | { 12 | result, 13 | error 14 | }: 15 | | { 16 | error: E 17 | result: undefined 18 | } 19 | | { 20 | error: undefined 21 | result: T 22 | }, 23 | eitherMode: EitherMode = "standard" 24 | ): TEitherMode extends "standard" 25 | ? StandardEither 26 | : TEitherMode extends "go" 27 | ? GoEither 28 | : AnyEither => { 29 | if (error) { 30 | const array = eitherMode === "standard" ? [error, undefined] : [undefined, error] 31 | const obj = {} as any 32 | obj.error = error 33 | obj.result = undefined 34 | return createArrayProxy(obj, array) 35 | } 36 | const array = eitherMode === "standard" ? [undefined, result] : [result, undefined] 37 | const obj = {} as any 38 | obj.error = undefined 39 | obj.result = result 40 | return createArrayProxy(obj, array) 41 | } 42 | 43 | const createArrayProxy = (obj: any, array: (undefined | Error | T)[]) => { 44 | // Proxy to intercept array methods and properties 45 | return new Proxy(obj, { 46 | get(target, prop, receiver) { 47 | // If the property exists on the object itself, return it 48 | if (prop in target) { 49 | return Reflect.get(target, prop, receiver) 50 | } 51 | 52 | // If the property exists on the internal array, proxy it 53 | if (prop in array) { 54 | const value = (array as any)[prop] // TypeScript array typing here 55 | if (typeof value === "function") { 56 | // Proxy array methods 57 | return function (...args: any[]) { 58 | return value.apply(array, args) 59 | } 60 | } else { 61 | // Return array properties (like length) 62 | return value 63 | } 64 | } 65 | 66 | // Handle the iterator separately 67 | if (prop === Symbol.iterator) { 68 | const originalIterator = array[Symbol.iterator]() 69 | return function* () { 70 | for (let item of originalIterator) { 71 | yield item 72 | } 73 | } 74 | } 75 | 76 | return undefined 77 | } 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /src/utils/errors.ts: -------------------------------------------------------------------------------- 1 | export function handleError(error: unknown): E { 2 | if (error instanceof Error) { 3 | return error as E 4 | } 5 | if (typeof error === "string") { 6 | return createErrorWithoutMightFailStackTraces(error) as E 7 | } 8 | if (typeof error === "object" && error !== null) { 9 | if ("message" in error && typeof error.message === "string") { 10 | return createErrorWithoutMightFailStackTraces(error.message) as E 11 | } 12 | return createErrorWithoutMightFailStackTraces(error as any) as E 13 | } 14 | return createErrorWithoutMightFailStackTraces("Unknown error") as E 15 | } 16 | 17 | function createErrorWithoutMightFailStackTraces(message: any): Error { 18 | const error = new Error(message) 19 | 20 | const stack = error.stack?.split("\n") 21 | stack?.splice(1, 3) 22 | error.stack = stack?.join("\n") 23 | 24 | return error 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/mightFailFunction.ts: -------------------------------------------------------------------------------- 1 | import { type Either } from "../Either" 2 | import { handleError } from "./errors" 3 | import { createEither } from "./createEither" 4 | import { MightFailFunction } from "./utils.type" 5 | 6 | export const mightFailFunction: MightFailFunction<"standard"> = async function ( 7 | promise: T 8 | ): Promise, E>> { 9 | try { 10 | const result = await promise 11 | return createEither, E>({ result, error: undefined }) 12 | } catch (err) { 13 | const error = handleError(err) 14 | return createEither, E>({ error, result: undefined }) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/staticMethodsProxy.ts: -------------------------------------------------------------------------------- 1 | import { EitherMode, MightFailFunction } from "./utils.type" 2 | 3 | export const makeProxyHandler = >( 4 | mightFailFunction: TMightFailFunction 5 | ) => ({ 6 | get(_: TMightFailFunction, property: string) { 7 | if (Object.getOwnPropertyDescriptor(Promise, property) === undefined) { 8 | return mightFailFunction(Promise.reject(new Error(`property ${property} not found on Promise`))) 9 | } 10 | 11 | const value = (Promise as any)[property] 12 | 13 | if (typeof value !== "function") { 14 | return mightFailFunction(Promise.reject(new Error(`property ${property} is not a Promise method`))) 15 | } 16 | 17 | return function (...args: any[]) { 18 | const promise = value.apply(Promise, args) 19 | return mightFailFunction(promise) 20 | } 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /src/utils/utils.type.ts: -------------------------------------------------------------------------------- 1 | import type { Either as StandardEither } from "../Either" 2 | import type { Either as GoEither } from "../go/Either" 3 | 4 | export type EitherMode = "standard" | "go" | "any" 5 | 6 | export type AnyEither = StandardEither | GoEither 7 | 8 | export type MightFailFunction = ( 9 | promise: T 10 | ) => Promise< 11 | TEitherMode extends "standard" 12 | ? StandardEither, E> 13 | : TEitherMode extends "go" 14 | ? GoEither, E> 15 | : AnyEither, E> 16 | > 17 | 18 | export type PromiseFulfilledResult = { 19 | status: "fulfilled" 20 | value: T 21 | } 22 | export type PromiseRejectedResult = { 23 | status: "rejected" 24 | reason: any 25 | } 26 | export type PromiseSettledResult = PromiseFulfilledResult | PromiseRejectedResult 27 | 28 | export type MightFail< 29 | TEitherMode extends EitherMode, 30 | TPromiseStaticMethods = PromiseStaticMethods 31 | > = MightFailFunction & TPromiseStaticMethods 32 | 33 | export interface PromiseStaticMethods { 34 | /** 35 | * Wraps a Promise.all call in a mightFail function. 36 | * @param values - An iterable of promises 37 | * @template T The type of the resolved values 38 | * @return {Promise} - Promise>> 39 | */ 40 | all( 41 | values: T 42 | ): Promise< 43 | TEitherMode extends "standard" 44 | ? Awaited }>> 45 | : TEitherMode extends "go" 46 | ? Awaited }>> 47 | : Awaited }>> 48 | > 49 | 50 | /** 51 | * (From lib.es2025.iterable.d.ts) 52 | * Wraps a Promise.all call in a mightFail function. 53 | * @param values - An iterable of promises 54 | * @template T The type of the resolved values 55 | * @return {Promise} - Promise>> 56 | */ 57 | all( 58 | values: Iterable> 59 | ): Promise< 60 | TEitherMode extends "standard" 61 | ? Awaited> 62 | : TEitherMode extends "go" 63 | ? Awaited> 64 | : Awaited> 65 | > 66 | 67 | /** 68 | * Wraps a Promise.race call in a mightFail function. 69 | * 70 | * @param values - An array of promises 71 | * @template T The type of the resolved values 72 | * @return {Promise} - Promise>> 73 | */ 74 | race( 75 | values: Iterable> 76 | ): Promise< 77 | TEitherMode extends "standard" 78 | ? Awaited> 79 | : TEitherMode extends "go" 80 | ? Awaited> 81 | : Awaited> 82 | > 83 | 84 | /** 85 | * Wraps a Promise.race call in a mightFail function. 86 | * @param values - An array of promises 87 | * @template T The type of the resolved values 88 | * @return {Promise} - Promise>> 89 | */ 90 | race( 91 | values: T 92 | ): Promise< 93 | TEitherMode extends "standard" 94 | ? Awaited> 95 | : TEitherMode extends "go" 96 | ? Awaited> 97 | : Awaited> 98 | > 99 | 100 | /** 101 | * Wraps a Promise.any call in a mightFail function. 102 | * 103 | * @param values - An array of promises 104 | * @template T The type of the resolved values 105 | * @return {Promise} - Promise>> 106 | */ 107 | any( 108 | values: T 109 | ): Promise< 110 | TEitherMode extends "standard" 111 | ? StandardEither, AggregateError> 112 | : TEitherMode extends "go" 113 | ? GoEither, AggregateError> 114 | : AnyEither, AggregateError> 115 | > 116 | 117 | /** 118 | * Wraps a Promise.any call in a mightFail function. 119 | * 120 | * @param values - An iterable of promises 121 | * @template T The type of the resolved values 122 | * @return {Promise} - Promise>> 123 | */ 124 | any( 125 | values: Iterable> 126 | ): Promise< 127 | TEitherMode extends "standard" 128 | ? StandardEither, AggregateError> 129 | : TEitherMode extends "go" 130 | ? GoEither, AggregateError> 131 | : AnyEither, AggregateError> 132 | > 133 | } 134 | -------------------------------------------------------------------------------- /test/go/makeMightFail.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest" 2 | import { makeMightFail } from "../../src/go/index" 3 | 4 | test("success returns the response", async () => { 5 | const resolve = (value: { message: string }) => Promise.resolve(value) 6 | const func = makeMightFail(resolve) 7 | const [result, error] = await func({ message: "success" }) 8 | expect(error).toBe(undefined) 9 | expect(result!.message).toBe("success") 10 | }) 11 | 12 | test("fail with error returns the error", async () => { 13 | const reject = (error: Error) => Promise.reject(error) 14 | const func = makeMightFail(reject) 15 | const [result, error] = await func(new Error("error")) 16 | expect(result).toBe(undefined) 17 | expect(error?.message).toBe("error") 18 | }) 19 | 20 | test("fail without error returns an error", async () => { 21 | const reject = () => Promise.reject(undefined) 22 | const func = makeMightFail(reject) 23 | const [result, error] = await func() 24 | expect(result).toBe(undefined) 25 | expect(error?.message).toBeTruthy() 26 | }) 27 | -------------------------------------------------------------------------------- /test/go/makeMightFailSync.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest" 2 | import { makeMightFailSync } from "../../src/go/index" 3 | 4 | function somethingThatThrows(input: string) { 5 | if (!input) { 6 | throw new Error("error") 7 | } 8 | return { message: input } 9 | } 10 | 11 | test("success returns the response", async () => { 12 | const func = makeMightFailSync(somethingThatThrows) 13 | const [result, error] = await func("success") 14 | expect(error).toBe(undefined) 15 | expect(result!.message).toBe("success") 16 | }) 17 | 18 | test("fail with error returns the error", async () => { 19 | const func = makeMightFailSync(somethingThatThrows) 20 | const [result, error] = await func("") 21 | expect(result).toBe(undefined) 22 | expect(error?.message).toBe("error") 23 | }) 24 | 25 | test("fail without error returns an error", async () => { 26 | const reject = () => { 27 | throw "a fit" 28 | } 29 | const func = makeMightFailSync(reject) 30 | const [result, error] = await func() 31 | expect(result).toBe(undefined) 32 | expect(error?.message).toBeTruthy() 33 | }) 34 | -------------------------------------------------------------------------------- /test/go/mightFail.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, test } from "vitest" 2 | import { mightFail, Might, Fail } from "../../src/go/index" 3 | 4 | test("success returns the response", async () => { 5 | const [result, error] = await mightFail(Promise.resolve("success")) 6 | expect(result).toBe("success") 7 | expect(error).toBe(undefined) 8 | }) 9 | 10 | test("fail with error returns the error", async () => { 11 | const [result, error] = await mightFail(Promise.reject(new Error("error"))) 12 | expect(result).toBe(undefined) 13 | expect(error?.message).toBe("error") 14 | }) 15 | 16 | test("fail without error returns an error", async () => { 17 | const [result, error] = await mightFail(Promise.reject(undefined)) 18 | expect(result).toBe(undefined) 19 | expect(error?.message).toBeTruthy() 20 | }) 21 | 22 | test("fail with string returns an error with that string as the message", async () => { 23 | const [result, error] = await mightFail(Promise.reject("error")) 24 | expect(result).toBe(undefined) 25 | expect(error?.message).toBe("error") 26 | }) 27 | 28 | test("fail with object returns an error with that object as the message", async () => { 29 | const [result, error] = await mightFail(Promise.reject({ message: "error" })) 30 | expect(result).toBe(undefined) 31 | expect(error?.message).toBe("error") 32 | }) 33 | 34 | test("async function that throws", async () => { 35 | const asyncFn = async () => { 36 | throw new Error("async error") 37 | } 38 | const [result, error] = await mightFail(asyncFn()) 39 | expect(result).toBe(undefined) 40 | expect(error?.message).toBe("async error") 41 | }) 42 | 43 | test("promise that resolves after delay", async () => { 44 | const delayedPromise = new Promise((resolve) => setTimeout(() => resolve("delayed success"), 100)) 45 | const [result, error] = await mightFail(delayedPromise) 46 | expect(result).toBe("delayed success") 47 | expect(error).toBe(undefined) 48 | }) 49 | 50 | test("promise that rejects after delay", async () => { 51 | const delayedPromise = new Promise((_, reject) => setTimeout(() => reject("delayed error"), 100)) 52 | const [result, error] = await mightFail(delayedPromise) 53 | expect(result).toBe(undefined) 54 | expect(error?.message).toBe("delayed error") 55 | }) 56 | 57 | describe("promise concurrent method wrappers", () => { 58 | describe("mightFail.all", () => { 59 | it("should resolve with all values when all promises succeed", async () => { 60 | const promises = [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)] 61 | const [result, error] = await mightFail.all(promises) 62 | expect(result).toEqual([1, 2, 3]) 63 | expect(error).toBeUndefined() 64 | }) 65 | 66 | it("should return an error if any promise fails", async () => { 67 | const promises = [Promise.resolve(1), Promise.reject(new Error("Test Error")), Promise.resolve(3)] 68 | const [result, error] = await mightFail.all(promises) 69 | expect(result).toBeUndefined() 70 | expect(error).toBeInstanceOf(Error) 71 | expect(error!.message).toBe("Test Error") 72 | }) 73 | }) 74 | 75 | describe("mightFail.race", () => { 76 | it("should resolve with the first resolved value", async () => { 77 | const promises = [Promise.resolve(42), new Promise((resolve) => setTimeout(() => resolve(100), 100))] 78 | const [result, error] = await mightFail.race(promises) 79 | expect(result).toBe(42) 80 | expect(error).toBeUndefined() 81 | }) 82 | 83 | it("should return an error if the first promise to settle is a rejection", async () => { 84 | const promises = [Promise.reject(new Error("Race Error")), Promise.resolve(100)] 85 | const [result, error] = await mightFail.race(promises) 86 | expect(result).toBeUndefined() 87 | expect(error).toBeInstanceOf(Error) 88 | expect(error!.message).toBe("Race Error") 89 | }) 90 | }) 91 | 92 | describe("mightFail.any", () => { 93 | it("should resolve with the first successful promise", async () => { 94 | const promises = [ 95 | Promise.reject(new Error("Error 1")), 96 | Promise.resolve(200), 97 | Promise.reject(new Error("Error 2")) 98 | ] 99 | const [result, error] = await mightFail.any(promises) 100 | expect(result).toBe(200) 101 | expect(error).toBeUndefined() 102 | }) 103 | 104 | it("should return an AggregateError if all promises fail", async () => { 105 | const promises = [Promise.reject(new Error("Error 1")), Promise.reject(new Error("Error 2"))] 106 | const [result, error] = await mightFail.any(promises) 107 | expect(result).toBeUndefined() 108 | expect(error).toBeInstanceOf(Error) 109 | expect(error!.message).toBe("All promises were rejected") 110 | }) 111 | }) 112 | }) 113 | 114 | describe("Either factories (Might & Fail)", () => { 115 | describe("Might", () => { 116 | it("should return an Either with the value as the result and undefined as the error", () => { 117 | const [result, error] = Might(5) 118 | expect(result).toEqual(5) 119 | expect(error).toEqual(undefined) 120 | }) 121 | }) 122 | describe("Fail", () => { 123 | it("should return an Either with undefined as the result and the error as the error", () => { 124 | const error = new Error("error") 125 | const [failResult, failError] = Fail(error) 126 | expect(failResult).toEqual(undefined) 127 | expect(failError).toEqual(error) 128 | }) 129 | 130 | it("should return an Either with undefined as the result and the error must be an instance of Error", () => { 131 | const error = "error" 132 | const [failResult, failError] = Fail(error) 133 | expect(failResult).toEqual(undefined) 134 | expect(failError).toEqual(new Error(error)) 135 | }) 136 | }) 137 | }) 138 | -------------------------------------------------------------------------------- /test/go/mightFailSync.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest" 2 | import { mightFailSync } from "../../src/go/index" 3 | 4 | function somethingThatThrows(input: string) { 5 | if (!input) { 6 | throw new Error("error") 7 | } 8 | return { message: input } 9 | } 10 | 11 | test("success returns the response", async () => { 12 | const [result, error] = mightFailSync(() => somethingThatThrows("success")) 13 | expect(error).toBe(undefined) 14 | expect(result?.message).toBe("success") 15 | }) 16 | 17 | test("fail with error returns the error", async () => { 18 | const [result, error] = mightFailSync(() => somethingThatThrows("")) 19 | expect(result).toBe(undefined) 20 | expect(error?.message).toBe("error") 21 | }) 22 | 23 | test("fail without error returns an error", async () => { 24 | const [result, error] = await mightFailSync(() => { 25 | throw "a fit" 26 | }) 27 | expect(result).toBe(undefined) 28 | expect(error?.message).toBeTruthy() 29 | }) 30 | 31 | test("fail with string returns an error with that string as the message", async () => { 32 | const [result, error] = await mightFailSync(() => { 33 | throw "a fit" 34 | }) 35 | expect(result).toBe(undefined) 36 | expect(error?.message).toBe("a fit") 37 | }) 38 | -------------------------------------------------------------------------------- /test/handleError.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest" 2 | import { mightFail, mightFailSync } from "../src" 3 | 4 | test("must modify the stack trace", async () => { 5 | const { error } = await mightFail(Promise.reject("error")) 6 | 7 | expect(error).toBeDefined() 8 | expect(error?.stack).toBeDefined() 9 | 10 | const stack = error?.stack?.split("\n") 11 | expect(stack?.length).toBeGreaterThan(1) 12 | expect(stack?.[0]).toBe("Error: error") 13 | expect(stack?.[1]).include("at processTicksAndRejections") 14 | }) 15 | 16 | test("must not reveal might-fail trace when thrown from a function", async () => { 17 | async function throwError() { 18 | function throwErrorSync() { 19 | throw "error" 20 | } 21 | 22 | throwErrorSync() 23 | } 24 | 25 | const { error } = await mightFail(throwError()) 26 | 27 | const stack = error?.stack?.split("\n") 28 | expect(stack?.length).toBeGreaterThan(1) 29 | expect(stack?.[0]).toBe("Error: error") 30 | expect(stack?.[1]).include("at processTicksAndRejections") 31 | }) 32 | 33 | test("must not reveal might-fail trace when used with mightFailSync", async () => { 34 | function throwError() { 35 | function throwErrorSync() { 36 | throw "error" 37 | } 38 | 39 | throwErrorSync() 40 | } 41 | 42 | const { error } = mightFailSync(throwError) 43 | 44 | const stack = error?.stack?.split("\n") 45 | expect(stack?.length).toBeGreaterThan(1) 46 | expect(stack?.[0]).toBe("Error: error") 47 | expect(stack?.[1]).include("handleError.test.ts") 48 | }) 49 | 50 | test("must not reveal might-fail trace when passed an object", async () => { 51 | function throwError() { 52 | function throwErrorSync() { 53 | throw { message: "error" } 54 | } 55 | 56 | throwErrorSync() 57 | } 58 | 59 | const { error } = mightFailSync(throwError) 60 | 61 | const stack = error?.stack?.split("\n") 62 | expect(stack?.length).toBeGreaterThan(1) 63 | expect(stack?.[0]).toBe("Error: error") 64 | expect(stack?.[1]).include("handleError.test.ts") 65 | 66 | async function throwErrorAsync() { 67 | function throwErrorSync() { 68 | throw { message: "error" } 69 | } 70 | 71 | throwErrorSync() 72 | } 73 | 74 | const { error: errorAsync } = await mightFail(throwErrorAsync()) 75 | 76 | const stack2 = errorAsync?.stack?.split("\n") 77 | expect(stack2?.length).toBeGreaterThan(1) 78 | expect(stack2?.[0]).toBe("Error: error") 79 | expect(stack2?.[1]).include("at processTicksAndRejections") 80 | }) 81 | 82 | test("must not reveal might-fail trace when passed an exotic object", async () => { 83 | function throwError() { 84 | function throwErrorSync() { 85 | throw { foo: "error", bar: "baz" } 86 | } 87 | 88 | throwErrorSync() 89 | } 90 | 91 | const { error } = mightFailSync(throwError) 92 | 93 | const stack = error?.stack?.split("\n") 94 | expect(stack?.length).toBeGreaterThan(1) 95 | expect(stack?.[0]).toBe("Error: [object Object]") 96 | expect(stack?.[1]).include("handleError.test.ts") 97 | 98 | async function throwErrorAsync() { 99 | function throwErrorSync() { 100 | throw { foo: "error", bar: "baz" } 101 | } 102 | 103 | throwErrorSync() 104 | } 105 | 106 | const { error: errorAsync } = await mightFail(throwErrorAsync()) 107 | 108 | const stack2 = errorAsync?.stack?.split("\n") 109 | expect(stack2?.length).toBeGreaterThan(1) 110 | expect(stack2?.[0]).toBe("Error: [object Object]") 111 | expect(stack2?.[1]).include("at processTicksAndRejections") 112 | }) 113 | 114 | test("must not reveal might-fail trace when passed an unknown/unexpected type", async () => { 115 | function throwError() { 116 | function throwErrorSync() { 117 | throw 4 118 | } 119 | 120 | throwErrorSync() 121 | } 122 | 123 | const { error } = mightFailSync(throwError) 124 | 125 | const stack = error?.stack?.split("\n") 126 | expect(stack?.length).toBeGreaterThan(1) 127 | expect(stack?.[0]).toBe("Error: Unknown error") 128 | expect(stack?.[1]).include("handleError.test.ts") 129 | 130 | async function throwErrorAsync() { 131 | function throwErrorSync() { 132 | throw 4 133 | } 134 | 135 | throwErrorSync() 136 | } 137 | 138 | const { error: errorAsync } = await mightFail(throwErrorAsync()) 139 | 140 | const stack2 = errorAsync?.stack?.split("\n") 141 | expect(stack2?.length).toBeGreaterThan(1) 142 | expect(stack2?.[0]).toBe("Error: Unknown error") 143 | expect(stack2?.[1]).include("at processTicksAndRejections") 144 | }) 145 | -------------------------------------------------------------------------------- /test/makeMightFail.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest" 2 | import { makeMightFail } from "../src/index" 3 | 4 | test("success returns the response", async () => { 5 | const resolve = (value: { message: string }) => Promise.resolve(value) 6 | const func = makeMightFail(resolve) 7 | const { error, result } = await func({ message: "success" }) 8 | expect(error).toBe(undefined) 9 | expect(result!.message).toBe("success") 10 | }) 11 | 12 | test("fail with error returns the error", async () => { 13 | const reject = (error: Error) => Promise.reject(error) 14 | const func = makeMightFail(reject) 15 | const { error, result } = await func(new Error("error")) 16 | expect(result).toBe(undefined) 17 | expect(error?.message).toBe("error") 18 | }) 19 | 20 | test("fail without error returns an error", async () => { 21 | const reject = () => Promise.reject(undefined) 22 | const func = makeMightFail(reject) 23 | const { error, result } = await func() 24 | expect(result).toBe(undefined) 25 | expect(error?.message).toBeTruthy() 26 | }) 27 | -------------------------------------------------------------------------------- /test/makeMightFailSync.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest" 2 | import { makeMightFailSync } from "../src/index" 3 | 4 | function somethingThatThrows(input: string) { 5 | if (!input) { 6 | throw new Error("error") 7 | } 8 | return { message: input } 9 | } 10 | 11 | test("success returns the response", async () => { 12 | const func = makeMightFailSync(somethingThatThrows) 13 | const { error, result } = await func("success") 14 | expect(error).toBe(undefined) 15 | expect(result!.message).toBe("success") 16 | }) 17 | 18 | test("fail with error returns the error", async () => { 19 | const func = makeMightFailSync(somethingThatThrows) 20 | const { error, result } = await func("") 21 | expect(result).toBe(undefined) 22 | expect(error?.message).toBe("error") 23 | }) 24 | 25 | test("fail without error returns an error", async () => { 26 | const reject = () => { 27 | throw "a fit" 28 | } 29 | const func = makeMightFailSync(reject) 30 | const { error, result } = await func() 31 | expect(result).toBe(undefined) 32 | expect(error?.message).toBeTruthy() 33 | }) 34 | -------------------------------------------------------------------------------- /test/mightFail.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, test } from "vitest" 2 | import { mightFail, Might, Fail } from "../src/index" 3 | 4 | test("success returns the response", async () => { 5 | const { error, result } = await mightFail(Promise.resolve("success")) 6 | expect(result).toBe("success") 7 | expect(error).toBe(undefined) 8 | }) 9 | 10 | test("fail with error returns the error", async () => { 11 | const { error, result } = await mightFail(Promise.reject(new Error("error"))) 12 | expect(result).toBe(undefined) 13 | expect(error?.message).toBe("error") 14 | }) 15 | 16 | test("fail without error returns an error", async () => { 17 | const { error, result } = await mightFail(Promise.reject(undefined)) 18 | expect(result).toBe(undefined) 19 | expect(error?.message).toBeTruthy() 20 | }) 21 | 22 | test("fail with string returns an error with that string as the message", async () => { 23 | const { error, result } = await mightFail(Promise.reject("error")) 24 | expect(result).toBe(undefined) 25 | expect(error?.message).toBe("error") 26 | }) 27 | 28 | test("fail with object returns an error with that object as the message", async () => { 29 | const { error, result } = await mightFail(Promise.reject({ message: "error" })) 30 | expect(result).toBe(undefined) 31 | expect(error?.message).toBe("error") 32 | }) 33 | 34 | test("async function that throws", async () => { 35 | const asyncFn = async () => { 36 | throw new Error("async error") 37 | } 38 | const { error, result } = await mightFail(asyncFn()) 39 | expect(result).toBe(undefined) 40 | expect(error?.message).toBe("async error") 41 | }) 42 | 43 | test("promise that resolves after delay", async () => { 44 | const delayedPromise = new Promise((resolve) => setTimeout(() => resolve("delayed success"), 100)) 45 | const { error, result } = await mightFail(delayedPromise) 46 | expect(result).toBe("delayed success") 47 | expect(error).toBe(undefined) 48 | }) 49 | 50 | test("promise that rejects after delay", async () => { 51 | const delayedPromise = new Promise((_, reject) => setTimeout(() => reject("delayed error"), 100)) 52 | const { error, result } = await mightFail(delayedPromise) 53 | expect(result).toBe(undefined) 54 | expect(error?.message).toBe("delayed error") 55 | }) 56 | 57 | describe("promise concurrent method wrappers", () => { 58 | describe("mightFail.all", () => { 59 | it("should resolve with an array of values when all promises resolve", async () => { 60 | const promises = [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)] 61 | const { result, error } = await mightFail.all(promises) 62 | expect(error).toBeUndefined() 63 | expect(result).toEqual([1, 2, 3]) 64 | }) 65 | 66 | it("should fail with the first error when any promise rejects", async () => { 67 | const promises = [Promise.resolve(1), Promise.reject(new Error("Error")), Promise.resolve(3)] 68 | const { result, error } = await mightFail.all(promises) 69 | expect(result).toBeUndefined() 70 | expect(error).toBeInstanceOf(Error) 71 | expect(error!.message).toBe("Error") 72 | }) 73 | }) 74 | 75 | describe("mightFail.race", () => { 76 | it("should resolve with the first resolved value", async () => { 77 | const promises = [Promise.resolve(1), Promise.reject(new Error("Error")), Promise.resolve(3)] 78 | const { result, error } = await mightFail.race(promises) 79 | expect(error).toBeUndefined() 80 | expect(result).toBe(1) 81 | }) 82 | 83 | it("should reject with the first rejected error if it occurs before any promise resolves", async () => { 84 | const promises = [Promise.reject(new Error("Race Error")), Promise.resolve(1), Promise.resolve(2)] 85 | const { result, error } = await mightFail.race(promises) 86 | expect(result).toBeUndefined() 87 | expect(error).toBeInstanceOf(Error) 88 | expect(error!.message).toBe("Race Error") 89 | }) 90 | }) 91 | 92 | describe("mightFail.any", () => { 93 | it("should resolve with the first fulfilled value", async () => { 94 | const promises = [Promise.reject(new Error("Error 1")), Promise.resolve(2), Promise.reject(new Error("Error 2"))] 95 | const { result, error } = await mightFail.any(promises) 96 | expect(error).toBeUndefined() 97 | expect(result).toBe(2) 98 | }) 99 | 100 | it("should reject with an AggregateError if all promises reject", async () => { 101 | const promises = [Promise.reject(new Error("Error 1")), Promise.reject(new Error("Error 2"))] 102 | const { result, error } = await mightFail.any(promises) 103 | expect(result).toBeUndefined() 104 | expect(error).toBeInstanceOf(Error) 105 | expect(error!.message).toBe("All promises were rejected") 106 | }) 107 | }) 108 | }) 109 | 110 | describe("Either factories (Might & Fail)", () => { 111 | describe("Might", () => { 112 | it("should return an Either with the value as the result and undefined as the error", () => { 113 | const result = Might(5) 114 | expect(result).toEqual({ 115 | result: 5, 116 | error: undefined 117 | }) 118 | }) 119 | }) 120 | describe("Fail", () => { 121 | it("should return an Either with undefined as the result and the error as the error", () => { 122 | const error = new Error("error") 123 | const result = Fail(error) 124 | expect(result).toEqual({ 125 | result: undefined, 126 | error 127 | }) 128 | }) 129 | 130 | it("should return an Either with undefined as the result and the error must be an instance of Error", () => { 131 | const error = "error" 132 | const result = Fail(error) 133 | expect(result).toEqual({ 134 | result: undefined, 135 | error: new Error(error) 136 | }) 137 | }) 138 | }) 139 | }) 140 | -------------------------------------------------------------------------------- /test/mightFailSync.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest" 2 | import { mightFailSync } from "../src/index" 3 | 4 | function somethingThatThrows(input: string) { 5 | if (!input) { 6 | throw new Error("error") 7 | } 8 | return { message: input } 9 | } 10 | 11 | test("success returns the response", async () => { 12 | const { error, result } = mightFailSync(() => somethingThatThrows("success")) 13 | expect(error).toBe(undefined) 14 | expect(result?.message).toBe("success") 15 | }) 16 | 17 | test("fail with error returns the error", async () => { 18 | const { error, result } = mightFailSync(() => somethingThatThrows("")) 19 | expect(result).toBe(undefined) 20 | expect(error?.message).toBe("error") 21 | }) 22 | 23 | test("fail without error returns an error", async () => { 24 | const { error, result } = await mightFailSync(() => { 25 | throw "a fit" 26 | }) 27 | expect(result).toBe(undefined) 28 | expect(error?.message).toBeTruthy() 29 | }) 30 | 31 | test("fail with string returns an error with that string as the message", async () => { 32 | const { error, result } = await mightFailSync(() => { 33 | throw "a fit" 34 | }) 35 | expect(result).toBe(undefined) 36 | expect(error?.message).toBe("a fit") 37 | }) 38 | -------------------------------------------------------------------------------- /test/tuple/makeMightFail.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest" 2 | import { makeMightFail } from "../../src/index" 3 | 4 | test("success returns the response", async () => { 5 | const resolve = (value: { message: string }) => Promise.resolve(value) 6 | const func = makeMightFail(resolve) 7 | const [error, result] = await func({ message: "success" }) 8 | expect(error).toBe(undefined) 9 | expect(result!.message).toBe("success") 10 | }) 11 | 12 | test("fail with error returns the error", async () => { 13 | const reject = (error: Error) => Promise.reject(error) 14 | const func = makeMightFail(reject) 15 | const [error, result] = await func(new Error("error")) 16 | expect(result).toBe(undefined) 17 | expect(error?.message).toBe("error") 18 | }) 19 | 20 | test("fail without error returns an error", async () => { 21 | const reject = () => Promise.reject(undefined) 22 | const func = makeMightFail(reject) 23 | const [error, result] = await func() 24 | expect(result).toBe(undefined) 25 | expect(error?.message).toBeTruthy() 26 | }) 27 | -------------------------------------------------------------------------------- /test/tuple/makeMightFailSync.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest" 2 | import { makeMightFailSync } from "../../src/index" 3 | 4 | function somethingThatThrows(input: string) { 5 | if (!input) { 6 | throw new Error("error") 7 | } 8 | return { message: input } 9 | } 10 | 11 | test("success returns the response", async () => { 12 | const func = makeMightFailSync(somethingThatThrows) 13 | const [error, result] = await func("success") 14 | expect(error).toBe(undefined) 15 | expect(result!.message).toBe("success") 16 | }) 17 | 18 | test("fail with error returns the error", async () => { 19 | const func = makeMightFailSync(somethingThatThrows) 20 | const [error, result] = await func("") 21 | expect(result).toBe(undefined) 22 | expect(error?.message).toBe("error") 23 | }) 24 | 25 | test("fail without error returns an error", async () => { 26 | const reject = () => { 27 | throw "a fit" 28 | } 29 | const func = makeMightFailSync(reject) 30 | const [error, result] = await func() 31 | expect(result).toBe(undefined) 32 | expect(error?.message).toBeTruthy() 33 | }) 34 | -------------------------------------------------------------------------------- /test/tuple/mightFail.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, test } from "vitest" 2 | import { mightFail, Might, Fail } from "../../src/index" 3 | 4 | test("success returns the response", async () => { 5 | const [error, result] = await mightFail(Promise.resolve("success")) 6 | expect(result).toBe("success") 7 | expect(error).toBe(undefined) 8 | }) 9 | 10 | test("fail with error returns the error", async () => { 11 | const [error, result] = await mightFail(Promise.reject(new Error("error"))) 12 | expect(result).toBe(undefined) 13 | expect(error?.message).toBe("error") 14 | }) 15 | 16 | test("fail without error returns an error", async () => { 17 | const [error, result] = await mightFail(Promise.reject(undefined)) 18 | expect(result).toBe(undefined) 19 | expect(error?.message).toBeTruthy() 20 | }) 21 | 22 | test("fail with string returns an error with that string as the message", async () => { 23 | const [error, result] = await mightFail(Promise.reject("error")) 24 | expect(result).toBe(undefined) 25 | expect(error?.message).toBe("error") 26 | }) 27 | 28 | test("fail with object returns an error with that object as the message", async () => { 29 | const [error, result] = await mightFail(Promise.reject({ message: "error" })) 30 | expect(result).toBe(undefined) 31 | expect(error?.message).toBe("error") 32 | }) 33 | 34 | test("async function that throws", async () => { 35 | const asyncFn = async () => { 36 | throw new Error("async error") 37 | } 38 | const [error, result] = await mightFail(asyncFn()) 39 | expect(result).toBe(undefined) 40 | expect(error?.message).toBe("async error") 41 | }) 42 | 43 | test("promise that resolves after delay", async () => { 44 | const delayedPromise = new Promise((resolve) => setTimeout(() => resolve("delayed success"), 100)) 45 | const [error, result] = await mightFail(delayedPromise) 46 | expect(result).toBe("delayed success") 47 | expect(error).toBe(undefined) 48 | }) 49 | 50 | test("promise that rejects after delay", async () => { 51 | const delayedPromise = new Promise((_, reject) => setTimeout(() => reject("delayed error"), 100)) 52 | const [error, result] = await mightFail(delayedPromise) 53 | expect(result).toBe(undefined) 54 | expect(error?.message).toBe("delayed error") 55 | }) 56 | 57 | describe("promise concurrent method wrappers", () => { 58 | describe("mightFail.all", () => { 59 | it("should resolve with all values when all promises succeed", async () => { 60 | const promises = [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)] 61 | const [error, result] = await mightFail.all(promises) 62 | expect(error).toBeUndefined() 63 | expect(result).toEqual([1, 2, 3]) 64 | }) 65 | 66 | it("should return an error if any promise fails", async () => { 67 | const promises = [Promise.resolve(1), Promise.reject(new Error("Test Error")), Promise.resolve(3)] 68 | const [error, result] = await mightFail.all(promises) 69 | expect(result).toBeUndefined() 70 | expect(error).toBeInstanceOf(Error) 71 | expect(error!.message).toBe("Test Error") 72 | }) 73 | }) 74 | 75 | describe("mightFail.race", () => { 76 | it("should resolve with the first resolved value", async () => { 77 | const promises = [Promise.resolve(42), new Promise((resolve) => setTimeout(() => resolve(100), 100))] 78 | const [error, result] = await mightFail.race(promises) 79 | expect(error).toBeUndefined() 80 | expect(result).toBe(42) 81 | }) 82 | 83 | it("should return an error if the first promise to settle is a rejection", async () => { 84 | const promises = [Promise.reject(new Error("Race Error")), Promise.resolve(100)] 85 | const [error, result] = await mightFail.race(promises) 86 | expect(result).toBeUndefined() 87 | expect(error).toBeInstanceOf(Error) 88 | expect(error!.message).toBe("Race Error") 89 | }) 90 | }) 91 | 92 | describe("mightFail.any", () => { 93 | it("should resolve with the first successful promise", async () => { 94 | const promises = [ 95 | Promise.reject(new Error("Error 1")), 96 | Promise.resolve(200), 97 | Promise.reject(new Error("Error 2")) 98 | ] 99 | const [error, result] = await mightFail.any(promises) 100 | expect(error).toBeUndefined() 101 | expect(result).toBe(200) 102 | }) 103 | 104 | it("should return an AggregateError if all promises fail", async () => { 105 | const promises = [Promise.reject(new Error("Error 1")), Promise.reject(new Error("Error 2"))] 106 | const [error, result] = await mightFail.any(promises) 107 | expect(result).toBeUndefined() 108 | expect(error).toBeInstanceOf(Error) 109 | expect(error!.message).toBe("All promises were rejected") 110 | }) 111 | }) 112 | }) 113 | 114 | describe("Either factories (Might & Fail)", () => { 115 | describe("Might", () => { 116 | it("should return an Either with the value as the result and undefined as the error", () => { 117 | const [errResult, resResult] = Might(5) 118 | expect(errResult).toEqual(undefined) 119 | expect(resResult).toEqual(5) 120 | }) 121 | }) 122 | describe("Fail", () => { 123 | it("should return an Either with undefined as the result and the error as the error", () => { 124 | const error = new Error("error") 125 | const [errResult, resResult] = Fail(error) 126 | expect(errResult).toEqual(error) 127 | expect(resResult).toEqual(undefined) 128 | }) 129 | 130 | it("should return an Either with undefined as the result and the error must be an instance of Error", () => { 131 | const error = "error" 132 | const [errResult, resResult] = Fail(error) 133 | expect(errResult).toEqual(new Error(error)) 134 | expect(resResult).toEqual(undefined) 135 | }) 136 | }) 137 | }) 138 | -------------------------------------------------------------------------------- /test/tuple/mightFailSync.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest" 2 | import { mightFailSync } from "../../src/index" 3 | 4 | function somethingThatThrows(input: string) { 5 | if (!input) { 6 | throw new Error("error") 7 | } 8 | return { message: input } 9 | } 10 | 11 | test("success returns the response", async () => { 12 | const [error, result] = mightFailSync(() => somethingThatThrows("success")) 13 | expect(error).toBe(undefined) 14 | expect(result?.message).toBe("success") 15 | }) 16 | 17 | test("fail with error returns the error", async () => { 18 | const [error, result] = mightFailSync(() => somethingThatThrows("")) 19 | expect(result).toBe(undefined) 20 | expect(error?.message).toBe("error") 21 | }) 22 | 23 | test("fail without error returns an error", async () => { 24 | const [error, result] = await mightFailSync(() => { 25 | throw "a fit" 26 | }) 27 | expect(result).toBe(undefined) 28 | expect(error?.message).toBeTruthy() 29 | }) 30 | 31 | test("fail with string returns an error with that string as the message", async () => { 32 | const [error, result] = await mightFailSync(() => { 33 | throw "a fit" 34 | }) 35 | expect(result).toBe(undefined) 36 | expect(error?.message).toBe("a fit") 37 | }) 38 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es2022", 5 | "lib": ["es2022", "dom"], 6 | "module": "ES2020", 7 | "rootDir": "./src/", 8 | "outDir": "./dist/", 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | }, 12 | "include": [ 13 | "src/**/*.ts" 14 | ], 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "./dist/cjs", 6 | "target": "es2022", 7 | "moduleResolution": "node" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "declaration": true, 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": false, 10 | "lib": ["es2022", "dom"], 11 | "declarationMap": true, 12 | "sourceMap": true, 13 | "outDir": "./dist", 14 | "rootDir": "./src", 15 | "allowSyntheticDefaultImports": true, 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | }, 19 | "include": [ 20 | "src/**/*", 21 | ], 22 | "exclude": [ 23 | "node_modules", 24 | "dist" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin": [ 3 | "typedoc-plugin-markdown" 4 | ], 5 | "out": "./docs/content/jsdocs", 6 | "outputFileStrategy": "members", 7 | "hidePageHeader": true, 8 | "hideBreadcrumbs": true, 9 | "useCodeBlocks": true, 10 | "expandObjects": true, 11 | "expandParameters": true, 12 | "parametersFormat": "table", 13 | "interfacePropertiesFormat": "table", 14 | "classPropertiesFormat": "table", 15 | "enumMembersFormat": "table", 16 | "typeDeclarationFormat": "table", 17 | "propertyMembersFormat": "table", 18 | "publicPath": "/", 19 | "sanitizeComments": true, 20 | "anchorPrefix": "markdown-header", 21 | "fileExtension": ".mdx", 22 | "entryFileName": "index", 23 | "readme": "none", 24 | "disableSources": true 25 | } --------------------------------------------------------------------------------