Skip to content

Commit

Permalink
Merge pull request #1 from meech-ward/main-1
Browse files Browse the repository at this point in the history
✨ Add support for sync error handling and streamline build process
  • Loading branch information
meech-ward committed Mar 16, 2024
2 parents 9169e87 + 3c6a463 commit 3649473
Show file tree
Hide file tree
Showing 9 changed files with 267 additions and 31 deletions.
24 changes: 24 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -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.
39 changes: 35 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
8 changes: 4 additions & 4 deletions src/Either.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
* @template T The type of the result value.
*/
type Either<T> =
| Promise<{
| {
error: Error;
result: undefined;
}>
| Promise<{
}
| {
result: T;
error: undefined;
}>;
};

export default Either;
8 changes: 4 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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 }
export { Either, mightFail, makeMightFail, mightFailSync, makeMightFailSync };
77 changes: 67 additions & 10 deletions src/makeMightFail.ts
Original file line number Diff line number Diff line change
@@ -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> = T extends Promise<infer U> ? 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<T>) => Either<UnwrapPromise<ReturnType<T>>>}
* 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<string> {
* 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<any>
>(
func: T
): (...funcArgs: Parameters<T>) => Either<UnwrapPromise<ReturnType<T>>> {
): (...funcArgs: Parameters<T>) => Promise<Either<UnwrapPromise<ReturnType<T>>>> {
return async (...args: Parameters<T>) => {
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<T>) => Either<ReturnType<T>> {
return (...args: Parameters<T>) => {
const throwingFunction = () => func(...args);
return mightFailSync(throwingFunction);
};
}
74 changes: 67 additions & 7 deletions src/mightFail.ts
Original file line number Diff line number Diff line change
@@ -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<T>} promise The promise to be wrapped in an Either.
* @return {Either<T>} A Promise that resolves with an Either.
* @param {Promise<T>} 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<Either<T>>} A Promise that resolves with an Either. This Either is a Success<T> 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<string> {
* 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<T>(
promise: Promise<T>
): Promise<Either<T>> {
export async function mightFail<T>(promise: Promise<T>): Promise<Either<T>> {
try {
const result = await promise;
return { error: undefined, result };
Expand All @@ -22,3 +43,42 @@ export default async function mightFail<T>(
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<T>} An object that is either a Success<T> with the result property set to the value returned by `func`,
* or a Failure with the error property set to the caught error. Success<T> 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<T>(func: () => T): Either<T> {
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,
};
}
}
34 changes: 34 additions & 0 deletions test/makeMightFailTry.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
29 changes: 29 additions & 0 deletions test/mightFailTry.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});

0 comments on commit 3649473

Please sign in to comment.