diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 0000000..bd45c8e --- /dev/null +++ b/Changelog.md @@ -0,0 +1,24 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [0.3.0] - 2024-03-16 + +### Added +- Support for synchronous error handling with `mightFailSync` and `makeMightFailSync` functions. +- A new `publish` script in `package.json` to streamline the build and publish process. + +### Changed +- 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. +- Updated `Either.ts` to streamline the type definition for a more straightforward implementation. + +## [0.1] - [0.2] + + +### Added +- Initial support for async error handling with `mightFail` and `makeMightFail` functions. +- Comprehensive documentation in the README, illustrating the use of the library with practical examples. +- Implementation of the `Either` type to support the async error handling pattern. + +### Changed +- Various internal improvements for better performance and reliability. diff --git a/README.md b/README.md index 8b27abf..084c554 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # Might Fail -A TypeScript library for handling async errors without `try` and `catch` blocks. Inspired by other languages that utilize Result or Either types for safer error handling. +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. The following examples are verbose to show how you would handle different types of errors differently instead of just catching all errors together and handling them in the same way. However, you can use `mightFail` to handle all errors in the same way if you want. -## Quick Start - -### Install +## Install ``` npm install might-fail ``` +## Async + ### Wrap Promise in `mightFail` ```ts @@ -66,6 +66,37 @@ const posts = result.data posts.map((post) => console.log(post.title)); ``` +## Sync + + +### Wrap throwing functions in `mightFailSync` + +```ts +const {error, result} = mightFailSync(() => JSON.parse("")); // JSON.parse might throw +if (error) { + console.error('Parsing failed:', error); + return +} +console.log('Parsed object:', result); +``` + +### Or Wrap Sync Function in `makeMightFailSync` + +```ts +function parseJSON(jsonString: string) { + return JSON.parse(jsonString); // This might throw +} +const safeParseJSON = makeMightFailSync(parseJSON); + +const { error, result } = safeParseJSON(""); + +if (error) { + console.error("Parsing failed:", error); + return; +} +console.log("Parsed object:", result); +``` + --- ## Either Type diff --git a/package.json b/package.json index a8907e0..8941384 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,14 @@ { "name": "might-fail", - "version": "0.2.1", + "version": "0.3.0", "description": "Return an Either object instead of throwing an exception", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { "build": "tsc", - "test": "vitest" + "test": "vitest", + "publish": "npm run build && npm publish" }, "files": [ "/dist" diff --git a/src/Either.ts b/src/Either.ts index eabfa06..3d048cb 100644 --- a/src/Either.ts +++ b/src/Either.ts @@ -6,13 +6,13 @@ * @template T The type of the result value. */ type Either = - | Promise<{ + | { error: Error; result: undefined; - }> - | Promise<{ + } + | { result: T; error: undefined; - }>; + }; export default Either; diff --git a/src/index.ts b/src/index.ts index 91039c8..ca31318 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ -import Either from "./Either" -import mightFail from "./mightFail" -import makeMightFail from "./makeMightFail" +import Either from "./Either"; +import { mightFail, mightFailSync } from "./mightFail"; +import { makeMightFail, makeMightFailSync } from "./makeMightFail"; -export { Either, mightFail, makeMightFail } \ No newline at end of file +export { Either, mightFail, makeMightFail, mightFailSync, makeMightFailSync }; diff --git a/src/makeMightFail.ts b/src/makeMightFail.ts index 2ecb486..fb33f38 100644 --- a/src/makeMightFail.ts +++ b/src/makeMightFail.ts @@ -1,30 +1,87 @@ import Either from "./Either"; -import mightFail from "./mightFail"; +import {mightFail, mightFailSync} from "./mightFail"; /** - * Utility type that unwraps a Promise type. If T is a Promise, it extracts the type the Promise resolves to. + * Utility type that unwraps a Promise type. If T is a Promise, it extracts the type the Promise resolves to, + * providing direct access to the underlying value type. * * @template T The type to be unwrapped if it's a Promise. */ type UnwrapPromise = T extends Promise ? U : T; /** - * Wraps a promise-returning function in a function that returns an Either. This function takes a function - * which returns a Promise, and returns a new function that when called, will return an Either. + * Wraps a promise-returning function in another function that instead of returning a Promise directly, + * returns a Promise that resolves with an Either. This allows for the handling of both resolved values and + * errors in a consistent, functional way. * * @export - * @template T The type of the function to be wrapped. - * @param {T} func The function to be wrapped. - * @return {(...funcArgs: Parameters) => Either>>} - * A new function that takes the same arguments as the original function, but returns an Either. + * @template T The function type that returns a Promise. + * @param {T} func - The async function to be wrapped. This function should return a Promise. + * @return {Function} A new function that, when called, returns a Promise that resolves with an Either object. + * The Either object contains either a 'result' with the resolved value of the original Promise, or an 'error' if the Promise was rejected. + * + * @example + * // Example of wrapping an async function that might fail: + * async function fetchData(url: string): Promise { + * const response = await fetch(url); + * if (!response.ok) { + * throw new Error('Network response was not ok'); + * } + * return response.text(); + * } + * + * const safeFetchData = makeMightFail(fetchData); + * const {error, result} = await safeFetchData('https://example.com'); + * + * if (error) { + * console.error('Fetching failed:', error.message); + * return + * } + * console.log('Fetched data:', result); */ -export default function makeMightFail< +export function makeMightFail< T extends (...args: any[]) => Promise >( func: T -): (...funcArgs: Parameters) => Either>> { +): (...funcArgs: Parameters) => Promise>>> { return async (...args: Parameters) => { const promise = func(...args); return mightFail(promise); }; } + +/** + * Wraps a synchronous function that might throw an exception in another function that, + * instead of throwing, returns an Either object. This object contains either a 'result' + * with the value returned by the function if it executes successfully, or an 'error' if the function throws. + * + * @export + * @template T The function type that might throw an error. + * @param {T} func - The function to be wrapped. This function might throw an exception. + * @return {Function} A new function that, when called, returns an Either object with either a 'result' or an 'error'. + * + * @example + * // Example of wrapping a synchronous function that might throw an error: + * function parseJSON(jsonString: string) { + * return JSON.parse(jsonString); // This might throw + * } + * + * const safeParseJSON = makeMightFailSync(parseJSON); + * const {error, result} = safeParseJSON('{"valid": "json"}'); + * + * if (error) { + * console.error('Parsing failed:', error); + * return; + * } + * console.log('Parsed object:', result); + */ +export function makeMightFailSync< + T extends (...args: any[]) => any +>( + func: T +): (...funcArgs: Parameters) => Either> { + return (...args: Parameters) => { + const throwingFunction = () => func(...args); + return mightFailSync(throwingFunction); + }; +} diff --git a/src/mightFail.ts b/src/mightFail.ts index 4391de0..a0db08b 100644 --- a/src/mightFail.ts +++ b/src/mightFail.ts @@ -1,17 +1,38 @@ import Either from "./Either"; /** - * Wraps a promise in an Either. This function takes a Promise of type T and returns a Promise - * which resolves with an object that either contains a 'result' of type T, or an 'error' of type Error. + * Wraps a promise in an Either to safely handle both its resolution and rejection. This function + * takes a Promise of type T and returns a Promise which resolves with an object. This object + * either contains a 'result' of type T if the promise resolves successfully, or an 'error' of type Error + * if the promise is rejected. * * @export * @template T The type of the result value. - * @param {Promise} promise The promise to be wrapped in an Either. - * @return {Either} A Promise that resolves with an Either. + * @param {Promise} promise - 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. + * @return {Promise>} A Promise that resolves with an Either. This Either is a Success with + * the 'result' property set to the value resolved by the promise if successful, and 'error' as undefined. + * 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. + * + * @example + * // Example of wrapping an async function that might fail: + * async function fetchData(url: string): Promise { + * const response = await fetch(url); + * if (!response.ok) { + * throw new Error('Network response was not ok'); + * } + * return response.text(); + * } + * + * const {error, result} = await mightFail(fetchData('https://example.com')); + * + * if (error) { + * console.error('Fetching failed:', error.message); + * return; + * } + * console.log('Fetched data:', result); */ -export default async function mightFail( - promise: Promise -): Promise> { +export async function mightFail(promise: Promise): Promise> { try { const result = await promise; return { error: undefined, result }; @@ -22,3 +43,42 @@ export default async function mightFail( return { error: new Error("Unknown error"), result: undefined }; } } + +/** + * Wraps a synchronous function in an Either type to safely handle exceptions. This function + * executes a provided function that returns a value of type T, capturing any thrown errors. + * It returns an object that either contains a 'result' of type T if the function succeeds, + * or an 'error' of type Error if the function throws an error. + * + * @export + * @template T The type of the result value. + * @param {() => T} func - A wrapper function that is expected to invoke the throwing function. + * That function should return a value of type T or throw an error. + * @return {Either} An object that is either a Success with the result property set to the value returned by `func`, + * or a Failure with the error property set to the caught error. Success has a 'result' of type T + * and 'error' as null. Failure has 'result' as null and 'error' of type Error. + * @example + * // Example of wrapping a synchronous function that might throw an error: + * const {error, result} = mightFailSync(() => JSON.parse("")); + * + * if (error) { + * console.error('Parsing failed:', error); + * return; + * } + * console.log('Parsed object:', result); + */ + +export function mightFailSync(func: () => T): Either { + try { + const result = func(); + return { error: undefined, result }; + } catch (error) { + if (error instanceof Error) { + return { error, result: undefined }; + } + return { + error: new Error("Unknown error: " + error.toString()), + result: undefined, + }; + } +} diff --git a/test/makeMightFailTry.test.ts b/test/makeMightFailTry.test.ts new file mode 100644 index 0000000..222ed85 --- /dev/null +++ b/test/makeMightFailTry.test.ts @@ -0,0 +1,34 @@ +import { expect, test } from "vitest" +import { makeMightFailSync } from "../src/index" + +function somethingThatThrows(input: string) { + if (!input) { + throw new Error("error") + } + return {message: input} +} + + +test("success returns the response", async () => { + const func = makeMightFailSync(somethingThatThrows) + const {error, result} = await func("success") + expect(error).toBe(undefined) + expect(result!.message).toBe("success") +}) + +test("fail with error returns the error", async () => { + const func = makeMightFailSync(somethingThatThrows) + const {error, result} = await func("") + expect(result).toBe(undefined) + expect(error?.message).toBe("error") +}) + +test("fail without error returns an error", async () => { + const reject = () => { + throw "a fit" + }; + const func = makeMightFailSync(reject) + const {error, result} = await func() + expect(result).toBe(undefined) + expect(error?.message).toBeTruthy() +}) \ No newline at end of file diff --git a/test/mightFailTry.test.ts b/test/mightFailTry.test.ts new file mode 100644 index 0000000..e4a6f16 --- /dev/null +++ b/test/mightFailTry.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from "vitest"; +import { mightFailSync } from "../src/index"; + +function somethingThatThrows(input: string) { + if (!input) { + throw new Error("error"); + } + return { message: input }; +} + +test("success returns the response", async () => { + const { error, result } = mightFailSync(() => somethingThatThrows("success")); + expect(error).toBe(undefined); + expect(result?.message).toBe("success"); +}); + +test("fail with error returns the error", async () => { + const { error, result } = mightFailSync(() => somethingThatThrows("")); + expect(result).toBe(undefined); + expect(error?.message).toBe("error"); +}); + +test("fail without error returns an error", async () => { + const { error, result } = await mightFailSync(() => { + throw "a fit"; + }); + expect(result).toBe(undefined); + expect(error?.message).toBeTruthy(); +});