Skip to content

Commit 10d1509

Browse files
committed
feat: allow returning Error instances in tryCatch
1 parent 275d88d commit 10d1509

File tree

4 files changed

+110
-5
lines changed

4 files changed

+110
-5
lines changed

.changeset/tender-dogs-tie.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@maxmorozoff/try-catch-tuple": minor
3+
---
4+
5+
feat: allow returning Error instances in tryCatch
6+
7+
- Enhanced `tryCatch` to support returning an `Error` instance instead of throwing it.

packages/try-catch-tuple/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,26 @@ console.log(nullError.cause); // null
103103

104104
This ensures tryCatch always provides a proper error object.
105105

106+
#### Return Error
107+
108+
You can also return an `Error` instance instead of throwing it:
109+
110+
```ts
111+
const [result, error] = tryCatch(() => {
112+
if (Math.random() < 0.5) {
113+
return new Error("Too small"); // Return Error instead of throwing it
114+
}
115+
116+
return { message: "ok" };
117+
});
118+
if (!error) return result;
119+
// ^? const result: { message: string }
120+
error;
121+
// ^? const error: Error
122+
result;
123+
// ^? const result: null
124+
```
125+
106126
#### Extending Error types
107127

108128
##### Option 1: Manually Set Result and Error Type

packages/try-catch-tuple/src/tryCatch.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ type DataErrorTuple<T, E> = Branded<
1010
/**
1111
* Represents a successful result where `data` is present and `error` is `null`.
1212
*/
13-
export type Success<T> = DataErrorTuple<T, null>;
13+
export type Success<T> = DataErrorTuple<Exclude<T, Error>, null>;
1414

1515
/**
1616
* Represents a failure result where `error` contains an error instance and `data` is `null`.
@@ -20,7 +20,9 @@ export type Failure<E extends Error> = DataErrorTuple<null, E | Error>;
2020
/**
2121
* Represents the result of an operation that can either succeed with `T` or fail with `E`.
2222
*/
23-
export type Result<T, E extends Error> = Success<T> | Failure<E>;
23+
export type Result<T, E extends Error> =
24+
| Success<T>
25+
| Failure<E | Extract<T, Error>>;
2426

2527
/**
2628
* Resolves the return type based on whether `T` is a promise:
@@ -107,7 +109,7 @@ export const tryCatch: TryCatch = <T, E extends Error = Error>(
107109
if (result instanceof Promise)
108110
return tryCatchAsync(result, operationName) as TryCatchResult<T, E>;
109111

110-
return [result, null] as TryCatchResult<T, E>;
112+
return handleResult<T, E>(result, operationName) as TryCatchResult<T, E>;
111113
} catch (rawError) {
112114
return handleError(rawError, operationName) as TryCatchResult<T, E>;
113115
}
@@ -119,7 +121,7 @@ export const tryCatchSync: TryCatch["sync"] = <T, E extends Error = Error>(
119121
) => {
120122
try {
121123
const result = fn();
122-
return [result, null] as Result<T, E>;
124+
return handleResult<T, E>(result, operationName);
123125
} catch (rawError) {
124126
return handleError(rawError, operationName);
125127
}
@@ -135,7 +137,7 @@ export const tryCatchAsync: TryCatch["async"] = async <
135137
try {
136138
const promise = typeof fn === "function" ? fn() : fn;
137139
const result = await promise;
138-
return [result, null] as Result<Awaited<T>, E>;
140+
return handleResult<Awaited<T>, E>(result, operationName);
139141
} catch (rawError) {
140142
return handleError(rawError, operationName);
141143
}
@@ -148,6 +150,22 @@ tryCatch.sync = tryCatchSync;
148150
tryCatch.async = tryCatchAsync;
149151
tryCatch.errors = tryCatchErrors;
150152

153+
// Handles a result which might be an error, annotates if needed
154+
function handleResult<T, E extends Error>(
155+
result: T,
156+
operationName?: string,
157+
): Result<T, E> {
158+
if (!isError(result)) {
159+
return [result, null] as Success<T>;
160+
}
161+
162+
if (operationName) {
163+
annotateErrorMessage(result, operationName);
164+
}
165+
166+
return [null, result] as Failure<Extract<T, E>>;
167+
}
168+
151169
// Handles a raw unknown error, ensures it's wrapped and annotated
152170
function handleError(rawError: unknown, operationName?: string) {
153171
const processedError = isError(rawError)

packages/try-catch-tuple/tests/tryCatch.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,66 @@ describe("tryCatch", () => {
400400
});
401401
});
402402

403+
describe("handleResult", () => {
404+
test("should handle returned Error", () => {
405+
const [result, error] = tryCatch(() => {
406+
return new Error("test");
407+
});
408+
expect(result).toBeNil();
409+
if (!error) expect.unreachable();
410+
expect(error).toBeInstanceOf(Error);
411+
expect(error.message).toBe("test");
412+
error satisfies Error;
413+
});
414+
415+
test("should handle returned async Error", async () => {
416+
const [result, error] = await tryCatch(async () => {
417+
return new Error("test");
418+
});
419+
expect(result).toBeNil();
420+
if (!error) expect.unreachable();
421+
expect(error).toBeInstanceOf(Error);
422+
expect(error.message).toBe("test");
423+
error satisfies Error;
424+
});
425+
426+
describe("should handle multiple errors", () => {
427+
/**
428+
* @note Explicit return type is required!
429+
*
430+
* TypeScript automatically collapses `SyntaxError` into `Error` because `SyntaxError` extends `Error`.
431+
* Without an explicit return type, TypeScript infers `Error | number`, discarding `SyntaxError`.
432+
*
433+
* By explicitly declaring `Error | SyntaxError | number`, we force TypeScript to retain `SyntaxError`
434+
* instead of generalizing it into `Error`.
435+
*
436+
* Use of `.error<E>()` helper also resolves this.
437+
*/
438+
const multipleErrorTypes = (n: number): Error | SyntaxError | number => {
439+
if (n === 0) {
440+
return new SyntaxError("test");
441+
}
442+
if (n === 1) {
443+
return new Error("test");
444+
}
445+
return 1;
446+
};
447+
448+
test.each([
449+
[0, SyntaxError],
450+
[1, Error],
451+
] as const)("should handle %s", (n, errorClass) => {
452+
const [result, error] = tryCatch(() => multipleErrorTypes(n));
453+
// ^?
454+
expect(result).toBeNil();
455+
if (!error) expect.unreachable();
456+
expect(error).toBeInstanceOf(errorClass);
457+
expect(error.message).toBe("test");
458+
error satisfies InstanceType<typeof errorClass>;
459+
});
460+
});
461+
});
462+
403463
describe("handleError", () => {
404464
const now = new Date();
405465
test.each([

0 commit comments

Comments
 (0)