Skip to content

Commit 85ac92c

Browse files
feat: add the withCatch function (#34)
1 parent 1ecf5ae commit 85ac92c

File tree

3 files changed

+151
-0
lines changed

3 files changed

+151
-0
lines changed

src/core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@ export {getStatus} from './utils/getStatus';
3030
export {hasTag} from './utils/hasTag';
3131
export {mergeStatuses} from './utils/mergeStatuses';
3232
export {skipContext} from './utils/skipContext';
33+
export {withCatch} from './utils/withCatch';
3334
export type {Cancellable} from './utils/withCancellation';
3435
export {isCancellable, isAbortable, withCancellation} from './utils/withCancellation';
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import {withCatch} from '../withCatch';
2+
3+
describe('withCatch', () => {
4+
it('should return the result of the fetch function when it succeeds', async () => {
5+
const mockFetch = jest.fn().mockResolvedValue({data: 'success'});
6+
const mockErrorHandler = jest.fn().mockReturnValue({error: 'handled'});
7+
8+
const safeFetch = withCatch(mockFetch, mockErrorHandler);
9+
const result = await safeFetch('arg1', 42);
10+
11+
expect(mockFetch).toHaveBeenCalledWith('arg1', 42);
12+
expect(mockErrorHandler).not.toHaveBeenCalled();
13+
expect(result).toEqual({data: 'success'});
14+
});
15+
16+
it('should call the error handler when the fetch function fails', async () => {
17+
const error = new Error('fetch failed');
18+
const mockFetch = jest.fn().mockRejectedValue(error);
19+
const mockErrorHandler = jest.fn().mockReturnValue({error: 'handled'});
20+
21+
const safeFetch = withCatch(mockFetch, mockErrorHandler);
22+
const result = await safeFetch('arg1', 42);
23+
24+
expect(mockFetch).toHaveBeenCalledWith('arg1', 42);
25+
expect(mockErrorHandler).toHaveBeenCalledWith(error);
26+
expect(result).toEqual({error: 'handled'});
27+
});
28+
29+
it('should work with functions that take no parameters', async () => {
30+
const mockFetch = jest.fn().mockResolvedValue({data: 'success'});
31+
const mockErrorHandler = jest.fn().mockReturnValue({error: 'handled'});
32+
33+
const safeFetch = withCatch(mockFetch, mockErrorHandler);
34+
const result = await safeFetch();
35+
36+
expect(mockFetch).toHaveBeenCalledWith();
37+
expect(mockErrorHandler).not.toHaveBeenCalled();
38+
expect(result).toEqual({data: 'success'});
39+
});
40+
41+
it('should work with functions that take multiple parameters', async () => {
42+
const mockFetch = jest.fn().mockResolvedValue({data: 'success'});
43+
const mockErrorHandler = jest.fn().mockReturnValue({error: 'handled'});
44+
45+
const safeFetch = withCatch(mockFetch, mockErrorHandler);
46+
const result = await safeFetch('arg1', 42, true, {complex: 'object'});
47+
48+
expect(mockFetch).toHaveBeenCalledWith('arg1', 42, true, {complex: 'object'});
49+
expect(mockErrorHandler).not.toHaveBeenCalled();
50+
expect(result).toEqual({data: 'success'});
51+
});
52+
53+
it('should handle error handlers that return promises', async () => {
54+
const error = new Error('fetch failed');
55+
const mockFetch = jest.fn().mockRejectedValue(error);
56+
const mockErrorHandler = jest.fn().mockResolvedValue({error: 'async handled'});
57+
58+
const safeFetch = withCatch(mockFetch, mockErrorHandler);
59+
const result = await safeFetch('arg1');
60+
61+
expect(mockFetch).toHaveBeenCalledWith('arg1');
62+
expect(mockErrorHandler).toHaveBeenCalledWith(error);
63+
expect(result).toEqual({error: 'async handled'});
64+
});
65+
66+
it('should preserve the type of the fetch function return value', async () => {
67+
interface User {
68+
id: number;
69+
name: string;
70+
}
71+
72+
const mockFetch = jest.fn().mockResolvedValue({
73+
id: 1,
74+
name: 'John Doe',
75+
} as User);
76+
77+
const mockErrorHandler = jest.fn().mockReturnValue(null);
78+
79+
const safeFetch = withCatch<[string], User, null>(mockFetch, mockErrorHandler);
80+
const result = await safeFetch('user1');
81+
82+
expect(result).toEqual({id: 1, name: 'John Doe'});
83+
// TypeScript should recognize result as User | null
84+
if (result !== null) {
85+
// This should compile without errors
86+
const userName = result.name;
87+
expect(userName).toBe('John Doe');
88+
}
89+
});
90+
91+
it('should preserve the type of the error handler return value', async () => {
92+
interface ErrorResponse {
93+
code: number;
94+
message: string;
95+
}
96+
97+
const error = new Error('fetch failed');
98+
const mockFetch = jest.fn<Promise<unknown>, [string]>().mockRejectedValue(error);
99+
100+
const mockErrorHandler = jest.fn().mockReturnValue({
101+
code: 500,
102+
message: 'Internal Server Error',
103+
} as ErrorResponse);
104+
105+
const safeFetch = withCatch<[string], unknown, ErrorResponse>(mockFetch, mockErrorHandler);
106+
const result = await safeFetch('user1');
107+
108+
expect(mockErrorHandler).toHaveBeenCalledWith(error);
109+
110+
// TypeScript should recognize result as unknown | ErrorResponse
111+
if (typeof result === 'object' && result !== null && 'code' in result) {
112+
// This should compile without errors
113+
const errorCode = (result as ErrorResponse).code;
114+
expect(errorCode).toBe(500);
115+
}
116+
});
117+
});

src/core/utils/withCatch.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* Creates a wrapper around a function that safely handles errors using a provided error handler.
3+
*
4+
* This utility function enhances a Promise-returning function by adding standardized error handling.
5+
* It catches any errors thrown by the original function and processes them through the provided
6+
* error handler, allowing for consistent error management across the application.
7+
*
8+
* @template TArgs - The argument types of the original function
9+
* @template TFetchReturnType - The return type of the original function's Promise
10+
* @template TCatchReturnType - The return type of the error handler function
11+
*
12+
* @param fetchFn - The original function that returns a Promise
13+
* @param onCatchFn - The error handler function that processes any caught errors
14+
*
15+
* @returns A new function with the same signature as the original function,
16+
* but with error handling applied. The returned function will resolve to either
17+
* the successful result of the original function or the result of the error handler.
18+
*
19+
* @example
20+
* // Basic usage with a string parameter
21+
* const fetchUser = withCatch(
22+
* someFetchFunction,
23+
* (error) => ({ error: true, message: error.message })
24+
* );
25+
*/
26+
export function withCatch<TArgs extends unknown[], TFetchReturnType, TCatchReturnType>(
27+
fetchFn: (...args: TArgs) => Promise<TFetchReturnType>,
28+
onCatchFn: (reason: unknown) => TCatchReturnType,
29+
): (...args: TArgs) => Promise<TFetchReturnType | TCatchReturnType> {
30+
return (...args) => {
31+
return fetchFn(...args).catch(onCatchFn) as Promise<TFetchReturnType | TCatchReturnType>;
32+
};
33+
}

0 commit comments

Comments
 (0)