Skip to content

Commit 3649473

Browse files
authored
Merge pull request #1 from meech-ward/main-1
✨ Add support for sync error handling and streamline build process
2 parents 9169e87 + 3c6a463 commit 3649473

9 files changed

+267
-31
lines changed

Changelog.md

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
## [0.3.0] - 2024-03-16
6+
7+
### Added
8+
- Support for synchronous error handling with `mightFailSync` and `makeMightFailSync` functions.
9+
- A new `publish` script in `package.json` to streamline the build and publish process.
10+
11+
### Changed
12+
- 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.
13+
- Updated `Either.ts` to streamline the type definition for a more straightforward implementation.
14+
15+
## [0.1] - [0.2]
16+
17+
18+
### Added
19+
- Initial support for async error handling with `mightFail` and `makeMightFail` functions.
20+
- Comprehensive documentation in the README, illustrating the use of the library with practical examples.
21+
- Implementation of the `Either` type to support the async error handling pattern.
22+
23+
### Changed
24+
- Various internal improvements for better performance and reliability.

README.md

+35-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
# Might Fail
22

3-
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.
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. 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.
44

5-
## Quick Start
6-
7-
### Install
5+
## Install
86

97
```
108
npm install might-fail
119
```
1210

11+
## Async
12+
1313
### Wrap Promise in `mightFail`
1414

1515
```ts
@@ -66,6 +66,37 @@ const posts = result.data
6666
posts.map((post) => console.log(post.title));
6767
```
6868

69+
## Sync
70+
71+
72+
### Wrap throwing functions in `mightFailSync`
73+
74+
```ts
75+
const {error, result} = mightFailSync(() => JSON.parse("")); // JSON.parse might throw
76+
if (error) {
77+
console.error('Parsing failed:', error);
78+
return
79+
}
80+
console.log('Parsed object:', result);
81+
```
82+
83+
### Or Wrap Sync Function in `makeMightFailSync`
84+
85+
```ts
86+
function parseJSON(jsonString: string) {
87+
return JSON.parse(jsonString); // This might throw
88+
}
89+
const safeParseJSON = makeMightFailSync(parseJSON);
90+
91+
const { error, result } = safeParseJSON("");
92+
93+
if (error) {
94+
console.error("Parsing failed:", error);
95+
return;
96+
}
97+
console.log("Parsed object:", result);
98+
```
99+
69100
---
70101

71102
## Either Type

package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
{
22
"name": "might-fail",
3-
"version": "0.2.1",
3+
"version": "0.3.0",
44
"description": "Return an Either object instead of throwing an exception",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
77

88
"scripts": {
99
"build": "tsc",
10-
"test": "vitest"
10+
"test": "vitest",
11+
"publish": "npm run build && npm publish"
1112
},
1213
"files": [
1314
"/dist"

src/Either.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
* @template T The type of the result value.
77
*/
88
type Either<T> =
9-
| Promise<{
9+
| {
1010
error: Error;
1111
result: undefined;
12-
}>
13-
| Promise<{
12+
}
13+
| {
1414
result: T;
1515
error: undefined;
16-
}>;
16+
};
1717

1818
export default Either;

src/index.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import Either from "./Either"
2-
import mightFail from "./mightFail"
3-
import makeMightFail from "./makeMightFail"
1+
import Either from "./Either";
2+
import { mightFail, mightFailSync } from "./mightFail";
3+
import { makeMightFail, makeMightFailSync } from "./makeMightFail";
44

5-
export { Either, mightFail, makeMightFail }
5+
export { Either, mightFail, makeMightFail, mightFailSync, makeMightFailSync };

src/makeMightFail.ts

+67-10
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,87 @@
11
import Either from "./Either";
2-
import mightFail from "./mightFail";
2+
import {mightFail, mightFailSync} from "./mightFail";
33

44
/**
5-
* Utility type that unwraps a Promise type. If T is a Promise, it extracts the type the Promise resolves to.
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.
67
*
78
* @template T The type to be unwrapped if it's a Promise.
89
*/
910
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
1011

1112
/**
12-
* Wraps a promise-returning function in a function that returns an Either. This function takes a function
13-
* which returns a Promise, and returns a new function that when called, will return an Either.
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.
1416
*
1517
* @export
16-
* @template T The type of the function to be wrapped.
17-
* @param {T} func The function to be wrapped.
18-
* @return {(...funcArgs: Parameters<T>) => Either<UnwrapPromise<ReturnType<T>>>}
19-
* A new function that takes the same arguments as the original function, but returns an Either.
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<string> {
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);
2041
*/
21-
export default function makeMightFail<
42+
export function makeMightFail<
2243
T extends (...args: any[]) => Promise<any>
2344
>(
2445
func: T
25-
): (...funcArgs: Parameters<T>) => Either<UnwrapPromise<ReturnType<T>>> {
46+
): (...funcArgs: Parameters<T>) => Promise<Either<UnwrapPromise<ReturnType<T>>>> {
2647
return async (...args: Parameters<T>) => {
2748
const promise = func(...args);
2849
return mightFail(promise);
2950
};
3051
}
52+
53+
/**
54+
* Wraps a synchronous function that might throw an exception in another function that,
55+
* instead of throwing, returns an Either object. This object contains either a 'result'
56+
* with the value returned by the function if it executes successfully, or an 'error' if the function throws.
57+
*
58+
* @export
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 object 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 {error, result} = 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<
79+
T extends (...args: any[]) => any
80+
>(
81+
func: T
82+
): (...funcArgs: Parameters<T>) => Either<ReturnType<T>> {
83+
return (...args: Parameters<T>) => {
84+
const throwingFunction = () => func(...args);
85+
return mightFailSync(throwingFunction);
86+
};
87+
}

src/mightFail.ts

+67-7
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,38 @@
11
import Either from "./Either";
22

33
/**
4-
* Wraps a promise in an Either. This function takes a Promise of type T and returns a Promise
5-
* which resolves with an object that either contains a 'result' of type T, or an 'error' of type Error.
4+
* Wraps a promise in an Either to safely handle both its resolution and rejection. This function
5+
* takes a Promise of type T and returns a Promise which resolves with an object. This object
6+
* either contains a 'result' of type T if the promise resolves successfully, or an 'error' of type Error
7+
* if the promise is rejected.
68
*
79
* @export
810
* @template T The type of the result value.
9-
* @param {Promise<T>} promise The promise to be wrapped in an Either.
10-
* @return {Either<T>} A Promise that resolves with an Either.
11+
* @param {Promise<T>} promise - The promise to be wrapped in an Either. This is an asynchronous operation that
12+
* should resolve with a value of type T or reject with an Error.
13+
* @return {Promise<Either<T>>} A Promise that resolves with an Either. This Either is a Success<T> with
14+
* the 'result' property set to the value resolved by the promise if successful, and 'error' as undefined.
15+
* 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.
16+
*
17+
* @example
18+
* // Example of wrapping an async function that might fail:
19+
* async function fetchData(url: string): Promise<string> {
20+
* const response = await fetch(url);
21+
* if (!response.ok) {
22+
* throw new Error('Network response was not ok');
23+
* }
24+
* return response.text();
25+
* }
26+
*
27+
* const {error, result} = await mightFail(fetchData('https://example.com'));
28+
*
29+
* if (error) {
30+
* console.error('Fetching failed:', error.message);
31+
* return;
32+
* }
33+
* console.log('Fetched data:', result);
1134
*/
12-
export default async function mightFail<T>(
13-
promise: Promise<T>
14-
): Promise<Either<T>> {
35+
export async function mightFail<T>(promise: Promise<T>): Promise<Either<T>> {
1536
try {
1637
const result = await promise;
1738
return { error: undefined, result };
@@ -22,3 +43,42 @@ export default async function mightFail<T>(
2243
return { error: new Error("Unknown error"), result: undefined };
2344
}
2445
}
46+
47+
/**
48+
* Wraps a synchronous function in an Either type to safely handle exceptions. This function
49+
* executes a provided function that returns a value of type T, capturing any thrown errors.
50+
* It returns an object that either contains a 'result' of type T if the function succeeds,
51+
* or an 'error' of type Error if the function throws an error.
52+
*
53+
* @export
54+
* @template T The type of the result value.
55+
* @param {() => T} func - A wrapper function that is expected to invoke the throwing function.
56+
* That function should return a value of type T or throw an error.
57+
* @return {Either<T>} An object that is either a Success<T> with the result property set to the value returned by `func`,
58+
* or a Failure with the error property set to the caught error. Success<T> has a 'result' of type T
59+
* and 'error' as null. Failure has 'result' as null and 'error' of type Error.
60+
* @example
61+
* // Example of wrapping a synchronous function that might throw an error:
62+
* const {error, result} = mightFailSync(() => JSON.parse(""));
63+
*
64+
* if (error) {
65+
* console.error('Parsing failed:', error);
66+
* return;
67+
* }
68+
* console.log('Parsed object:', result);
69+
*/
70+
71+
export function mightFailSync<T>(func: () => T): Either<T> {
72+
try {
73+
const result = func();
74+
return { error: undefined, result };
75+
} catch (error) {
76+
if (error instanceof Error) {
77+
return { error, result: undefined };
78+
}
79+
return {
80+
error: new Error("Unknown error: " + error.toString()),
81+
result: undefined,
82+
};
83+
}
84+
}

test/makeMightFailTry.test.ts

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
12+
test("success returns the response", async () => {
13+
const func = makeMightFailSync(somethingThatThrows)
14+
const {error, result} = await func("success")
15+
expect(error).toBe(undefined)
16+
expect(result!.message).toBe("success")
17+
})
18+
19+
test("fail with error returns the error", async () => {
20+
const func = makeMightFailSync(somethingThatThrows)
21+
const {error, result} = await func("")
22+
expect(result).toBe(undefined)
23+
expect(error?.message).toBe("error")
24+
})
25+
26+
test("fail without error returns an error", async () => {
27+
const reject = () => {
28+
throw "a fit"
29+
};
30+
const func = makeMightFailSync(reject)
31+
const {error, result} = await func()
32+
expect(result).toBe(undefined)
33+
expect(error?.message).toBeTruthy()
34+
})

test/mightFailTry.test.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
});

0 commit comments

Comments
 (0)