neat-catch
Version:
A comprehensive utility library for elegant error handling in TypeScript and JavaScript. Handle both sync and async operations with clean tuple-based results.
1,195 lines (914 loc) ⢠29.2 kB
Markdown
//img.shields.io/badge/Status-Active-green)
[](https://www.npmjs.com/package/neat-catch)

[](https://github.com/Dforrunner/neat-catch/blob/main/LICENSE)


A comprehensive utility library for elegant error handling in TypeScript and JavaScript. Transform messy try-catch blocks into clean, readable tuple-based results.
- **š Universal**: Handle both synchronous and asynchronous operations seamlessly
- **š¦ Type-safe**: Full TypeScript support with intelligent type inference
- **šØ Clean API**: Transform errors into readable `[data, error]` tuples
- **š Comprehensive**: Multiple utilities for different error handling scenarios
- **ā” Zero dependencies**: Lightweight and fast
- **š Universal**: Works in Node.js, browsers, and edge runtimes
```bash
pnpm install neat-catch
npm install neat-catch
yarn add neat-catch
```
```typescript
import { neatCatch } from "neat-catch";
// Synchronous operations
const [result, error] = neatCatch(() => JSON.parse('{"hello": "world"}'));
if (error) {
// ...handle error
}
// Asynchronous operations
const [data, fetchError] = await neatCatch(async () => {
const response = await fetch("/api/users");
return response.json();
});
if (fetchError) {
// ...handle error
}
```
<details>
<summary><code>neatCatch<TFn, E>(fn, errorTransformer?)</code></summary>
- `fn: TFn` - The sync or async function to execute
- `errorTransformer?: (error: unknown) => E` - Optional function to transform caught errors
- For sync functions: `[ReturnType<TFn>, null] | [null, E]`
- For async functions: `Promise<[Awaited<ReturnType<TFn>>, null] | [null, E]>`
A utility function that executes a sync or async function and returns a tuple where the first element is the data (or null if error) and the second element is the error (or null if successful).
### Examples
```typescript
// Synchronous function
const [data, error] = neatCatch(() => {
return JSON.parse('{"valid": "json"}');
});
if (error) {
console.error("Parse failed:", error);
} else {
console.log("Parsed data:", data);
}
// Asynchronous function
const [result, error] = await neatCatch(async () => {
const response = await fetch("/api/data");
return response.json();
});
// With error transformer
const [data, error] = await neatCatch(
async () => fetch("/api/data"),
(err) => `Network error: ${err}`
);
```
</details>
<details>
<summary><code>createNeatWrapper<TArgs, TReturn, E>(fn, errorTransformer?)</code></summary>
- `fn: (...args: TArgs) => TReturn | Promise<TReturn>` - The function to wrap (can be sync or async)
- `errorTransformer?: (error: unknown) => E` - Optional function to transform caught errors
- For sync functions: `<TOverride = TReturn>(...args: TArgs) => [TOverride, null] | [null, E]`
- For async functions: `<TOverride = TReturn>(...args: TArgs) => Promise<[TOverride, null] | [null, E]>`
Creates a wrapped version of a function that always returns a neat tuple when called.
```typescript
// Wrapping a synchronous function
const safeParse = createNeatWrapper(JSON.parse);
const [data, error] = safeParse('{"key": "value"}');
const [dataWithType, error2] = safeParse<{ key: "value" }>('{"key": "value"}');
// Wrapping an asynchronous function
const safeFetch = createNeatWrapper(fetch);
const [response, error] = await safeFetch("/api/data");
// With error transformer
const safeParseWithTransform = createNeatWrapper(
JSON.parse,
(err) => `JSON parsing failed: ${err}`
);
// Using with parameters
const safeCalculate = createNeatWrapper((a: number, b: number) => a / b);
const [result, error] = safeCalculate(10, 2); // [5, null]
const [result2, error2] = safeCalculate(10, 0); // [null, Error]
```
</details>
<details>
<summary><code>neatCatchAll<T, E>(operations, errorTransformer?)</code></summary>
- `operations: { [K in keyof T]: () => Promise<T[K]> }` - An array of functions returning Promises
- `errorTransformer?: (error: unknown) => E` - Optional function to transform caught errors
```typescript
Promise<{
results: { [K in keyof T]: T[K] | null } | null;
errors: { [K in keyof T]: E | null } | null;
}>;
```
Utility for handling multiple async operations and collecting results/errors. The index of results and errors corresponds to the index of the input operations.
Note that errors and results correspond to their index.
```typescript
// Multiple API calls
const [outcome] = await neatCatchAll([
() => fetch("/api/users").then((r) => r.json()),
() => fetch("/api/posts").then((r) => r.json()),
() => fetch("/api/comments").then((r) => r.json()),
]);
if (outcome.errors) {
console.log("Some operations failed:", outcome.errors);
//or handle specific errors. If there wasn't an error for the given fetch call it'll be 'undefined'
const [usersFetchError, postsFetchError, commentsFetchError] = outcome.errors;
if (usersFetchError) {
//handle error
}
if (postsFetchError) {
//handle error
}
if (commentsFetchError) {
//handle error
}
}
if (outcome.results) {
const [users, posts, comments] = outcome.results;
console.log("Success results:", { users, posts, comments });
}
// With error transformer
const { results, errors } = await neatCatchAll(
[() => Promise.resolve("success"), () => Promise.reject(new Error("failed"))],
(err) => `Transformed: ${err}`
);
```
</details>
<details>
<summary><code>neatCatchRetry<T, E>(fn, options?)</code></summary>
- `fn: () => Promise<T>` - The async function to execute
- `options?: NeatCatchRetryOptions<E>` - Configuration options for retries
```typescript
type NeatCatchRetryOptions<E = Error> = {
maxRetries?: number; // Default: 3
delay?: number; // Default: 1000ms
backoff?: "linear" | "exponential"; // Default: "exponential"
errorTransformer?: (error: unknown, attempt: number) => E;
shouldRetry?: (error: unknown, attempt: number) => boolean;
};
```
`Promise<[T, null] | [null, E]>`
Utility for retrying operations with neat error handling and configurable retry strategies.
```typescript
// Basic retry - will retry 3 times with 1s delay and exponential backoff by default
const [data, error] = await neatCatchRetry(async () => {
const response = await fetch("/api/unreliable-endpoint");
if (!response.ok) throw new Error("API failed");
return response.json();
});
// With custom options
const [result, error] = await neatCatchRetry(fetchDataFromApiFn, {
maxRetries: 5,
delay: 2000,
backoff: "linear",
shouldRetry: (error, attempt) => {
// Only retry on network errors
return error instanceof TypeError && attempt < 3;
},
errorTransformer: (error, attempt) => ({
message: `Failed after ${attempt} attempts: ${error}`,
attempt,
}),
});
```
</details>
<details>
<summary><code>errorTransformers.toString</code></summary>
- `error: unknown` - The error to transform
`string` - Error message as string
Transforms any error into a simple message string.
```typescript
const [data, error] = neatCatch((): void => {
throw new Error("Something went wrong");
}, errorTransformers.toString);
// error will be: "Something went wrong"
```
</details>
<details>
<summary><code>errorTransformers.toObject</code></summary>
- `error: unknown` - The error to transform
```typescript
{
message: string;
stack?: string;
name?: string;
cause?: unknown;
}
```
Transforms errors into a structured object with message and stack information.
```typescript
const [data, error] = neatCatch((): void => {
throw new TypeError("Invalid type");
}, errorTransformers.toObject);
// error will be: { message: "Invalid type", stack: "...", name: "TypeError" }
```
</details>
<details>
<summary><code>errorTransformers.withTimestamp</code></summary>
- `error: unknown` - The error to transform
```typescript
{
error: unknown;
timestamp: number;
isoString: string;
}
```
Transforms errors into a structured object with timestamp information.
```typescript
const [data, error] = neatCatch((): void => {
throw new Error("Timestamped error");
}, errorTransformers.withTimestamp);
// error will be: { error: Error, timestamp: 1638360000000, isoString: "2021-12-01T12:00:00.000Z" }
```
</details>
<details>
<summary><code>errorTransformers.fetchError</code></summary>
- `error: unknown` - The error to transform
```typescript
{
message: string;
status?: number;
statusText?: string;
url?: string;
isNetworkError: boolean;
isTimeout: boolean;
}
```
Transforms HTTP fetch errors into a structured format with network and timeout detection.
```typescript
const safeFetch = createNeatWrapper(fetch, errorTransformers.fetchError);
const [response, error] = await safeFetch("/api/data");
if (error) {
if (error.isNetworkError) {
console.log("Network issue detected");
}
if (error.status === 404) {
console.log("Resource not found");
}
}
```
</details>
<details>
<summary><code>errorTransformers.withContext</code></summary>
- `context: T` - Additional context to include with the error
Function that transforms error: `(error: unknown) => T & { error: unknown }`
Wraps an error with additional context information.
```typescript
const [data, error] = neatCatch(
() => processUserData(userId),
errorTransformers.withContext({
userId,
operation: "processUserData",
timestamp: Date.now(),
})
);
// error will include the context along with the original error
```
</details>
<details>
<summary><code>errorTransformers.toSimpleError</code></summary>
- `error: unknown` - The error to transform
`{ message: string }`
Simplifies errors by extracting only the message and discarding other properties.
```typescript
const [data, error] = neatCatch(() => {
throw new Error("Simple error");
}, errorTransformers.toSimpleError);
// error will be: { message: "Simple error" }
```
</details>
<details>
<summary><code>errorTransformers.forLogging</code></summary>
- `error: unknown` - The error to transform
```typescript
{
message: string;
stack?: string;
name?: string;
timestamp: number;
environment?: string;
}
```
Transforms errors for logging purposes with detailed information including environment context.
```typescript
const [data, error] = neatCatch((): void => {
throw new Error("Logging error");
}, errorTransformers.forLogging);
// error will include message, stack, timestamp, and NODE_ENV if available
```
</details>
<details>
<summary><code>NeatCatchResult<T, E></code></summary>
```typescript
type NeatCatchResult<T, E = Error> = [T, null] | [null, E];
```
Type utility representing the neat tuple result pattern used throughout the library.
```typescript
function processData(): NeatCatchResult<string> {
try {
return ["processed data", null];
} catch (error) {
return [null, error as Error];
}
}
```
</details>
<details>
<summary><code>NeatCatchRetryOptions<E></code></summary>
```typescript
type NeatCatchRetryOptions<E = Error> = {
maxRetries?: number;
delay?: number;
backoff?: "linear" | "exponential";
errorTransformer?: (error: unknown, attempt: number) => E;
shouldRetry?: (error: unknown, attempt: number) => boolean;
};
```
Configuration options for the `neatCatchRetry` function, allowing customization of retry behavior.
</details>
```typescript
import neatCatch from "neat-catch";
// Simple usage
const [data, error] = await neatCatch(async () => {
const response = await fetch("/api/data");
return response.json();
});
if (error) {
console.error("Operation failed:", error);
return;
}
console.log("Success:", data);
```
```typescript
import { createNeatWrapper, errorTransformers } from "neat-catch";
// Create reusable wrapped functions
const safeJsonParse = createNeatWrapper(JSON.parse, errorTransformers.toString);
const safeFetch = createNeatWrapper(fetch, errorTransformers.fetchError);
// Use throughout your application
const [parsed, parseError] = safeJsonParse(jsonString);
const [response, fetchError] = await safeFetch("/api/endpoint");
// Supports types
const [parsed, parseError] = safeJsonParse<JsonStringType>(jsonString);
const [response, fetchError] =
await safeFetch<ApiResponseType>("/api/endpoint");
```
```typescript
import { neatCatchAll } from "neat-catch";
// Handle multiple async operations
const { results, errors } = await neatCatchAll([
() => fetch("/api/users").then((r) => r.json()),
() => fetch("/api/posts").then((r) => r.json()),
() => fetchUserPreferences(userId),
]);
// Process results and errors with index correspondence
if (errors?.[0]) console.error("Users fetch failed:", errors[0]);
if (errors?.[1]) console.error("Posts fetch failed:", errors[1]);
if (results?.[0]) console.log("Users:", results[0]);
```
```typescript
import { neatCatchRetry } from "neat-catch";
// Retry with exponential backoff
const [data, error] = await neatCatchRetry(unstableApiCall, {
maxRetries: 3,
delay: 1000,
backoff: "exponential",
shouldRetry: (error) => error.name !== "ValidationError",
});
```
š” For api calls that utilize `fetch` I actually recommend checking out [neat-fetch](https://www.npmjs.com/package/neat-fetch) npm package. It works similar to neat-catch but it's specifically geared towards `fetch`.
```typescript
import { neatCatch, errorTransformers } from "neat-catch";
async function fetchUser(id: string) {
const [response, fetchError] = await neatCatch(
() => fetch(`/api/users/${id}`),
errorTransformers.fetchError
);
if (fetchError) {
if (fetchError.isNetworkError) {
// handle error
return;
}
// handle other errors
return;
}
const [userData, parseError] = await neatCatch(
() => response.json(),
errorTransformers.toString
);
if (parseError) {
// handle parse error
return;
}
return userData;
}
```
```typescript
import {
neatCatch,
errorTransformers,
neatCatchRetry,
createNeatWrapper,
} from "neat-catch";
// Database operations with connection handling
class DatabaseService {
constructor(private db: any) {} // Your database client
async findUser(id: string) {
const [user, error] = await neatCatch(
() => this.db.user.findUnique({ where: { id } }),
errorTransformers.withContext({ operation: "findUser", userId: id })
);
if (error) {
// Handle specific errors. I'm throwing them in these examples but you should probably handle them better.
if (error.error.code === "P2025") {
throw new Error(`User ${id} not found`);
}
if (error.error.code === "P1001") {
throw new Error("Database connection failed");
}
throw new Error(`Database error: ${error.error.message}`);
}
return user;
}
async createUserWithRetry(userData: any) {
const [user, error] = await neatCatchRetry(
() => this.db.user.create({ data: userData }),
{
maxRetries: 3,
delay: 1000,
shouldRetry: (error) => {
const dbError = error as any;
// Retry on connection issues, not on constraint violations
return dbError.code === "P1001" || dbError.code === "P1017";
},
errorTransformer: errorTransformers.forLogging,
}
);
if (error) {
console.error("Failed to create user after retries:", error);
throw new Error("Unable to create user - please try again later");
}
return user;
}
// Transaction with rollback handling
async transferFunds(fromId: string, toId: string, amount: number) {
const [result, error] = await neatCatch(
async () => {
return await this.db.$transaction(async (tx: any) => {
// Debit from source account
const [fromAccount, fromError] = await neatCatch(() =>
tx.account.update({
where: { id: fromId },
data: { balance: { decrement: amount } },
})
);
if (fromError) {
throw new Error(
`Failed to debit account ${fromId}: ${fromError.message}`
);
}
if (!fromAccount || fromAccount.balance < 0) {
throw new Error("Insufficient funds");
}
// Credit to destination account
const [toAccount] = await neatCatch(() =>
tx.account.update({
where: { id: toId },
data: { balance: { increment: amount } },
})
);
if (!toAccount) {
throw new Error("Destination account not found");
}
return { fromAccount, toAccount };
});
},
errorTransformers.withContext({
operation: "transferFunds",
fromId,
toId,
amount,
})
);
if (error) {
console.error("Transfer failed:", error);
throw new Error(`Transfer failed: ${error.error.message}`);
}
return result;
}
}
// Redis cache operations
class CacheService {
constructor(private redis: any) {}
async get<T>(key: string): Promise<T | null> {
const [data, error] = await neatCatch(async () => {
const result = await this.redis.get(key);
return result ? JSON.parse(result) : null;
}, errorTransformers.toString);
if (error) {
console.warn(`Cache get failed for key ${key}:`, error);
return null; // Graceful degradation
}
return data;
}
async set(key: string, value: any, ttlSeconds = 3600) {
const [, error] = await neatCatch(
() => this.redis.setex(key, ttlSeconds, JSON.stringify(value)),
errorTransformers.withTimestamp
);
if (error) {
console.error(`Cache set failed for key ${key}:`, error);
// Don't throw - cache failures shouldn't break the app
}
}
async invalidatePattern(pattern: string) {
const [, error] = await neatCatch(async () => {
const keys = await this.redis.keys(pattern);
if (keys.length > 0) {
await this.redis.del(...keys);
}
return keys.length;
}, errorTransformers.forLogging);
if (error) {
console.error(`Cache invalidation failed for pattern ${pattern}:`, error);
}
}
}
// File system operations
import { readFile, writeFile, mkdir, access } from "fs/promises";
import { dirname } from "path";
const safeReadFile = createNeatWrapper(readFile);
const safeWriteFile = createNeatWrapper(writeFile);
const safeMkdir = createNeatWrapper(mkdir);
async function processDataFile(inputPath: string, outputPath: string) {
// Check if input file exists
const [, accessError] = await neatCatch(
() => access(inputPath),
errorTransformers.toString
);
if (accessError) {
throw new Error(`Input file not found: ${inputPath}`);
}
// Read and parse data
const [rawData, readError] = await safeReadFile(inputPath, "utf8");
if (readError) {
throw new Error(`Failed to read file: ${readError.message}`);
}
const [parsedData, parseError] = await neatCatch(
() => JSON.parse(rawData),
errorTransformers.toObject
);
if (parseError) {
throw new Error(`Invalid JSON in ${inputPath}: ${parseError.message}`);
}
// Process data
const [processedData, processError] = await neatCatch(
async () => {
// Simulate async processing
await new Promise((resolve) => setTimeout(resolve, 100));
return parsedData.map((item: any) => ({
...item,
processed: true,
timestamp: Date.now(),
}));
},
errorTransformers.withContext({ operation: "dataProcessing" })
);
if (processError) {
throw new Error(`Data processing failed: ${processError.error}`);
}
// Ensure output directory exists
const [, mkdirError] = await safeMkdir(dirname(outputPath), {
recursive: true,
});
if (mkdirError) {
console.warn(`Failed to create directory: ${mkdirError.message}`);
}
// Write processed data
const [, writeError] = await safeWriteFile(
outputPath,
JSON.stringify(processedData, null, 2)
);
if (writeError) {
throw new Error(`Failed to write output: ${writeError.message}`);
}
return { processed: processedData.length, outputPath };
}
// Email service with queue processing
class EmailService {
constructor(
private emailClient: any,
private queue: any
) {}
async sendEmail(to: string, subject: string, body: string) {
const [result, error] = await neatCatchRetry(
() =>
this.emailClient.send({
to,
subject,
html: body,
from: "noreply@example.com",
}),
{
maxRetries: 3,
delay: 2000,
backoff: "exponential",
shouldRetry: (error) => {
const emailError = error as any;
// Retry on rate limits and server errors, not on invalid email
return emailError.statusCode >= 500 || emailError.statusCode === 429;
},
errorTransformer: errorTransformers.withContext({
operation: "sendEmail",
recipient: to,
}),
}
);
if (error) {
// Add to retry queue for later processing
const [, queueError] = await neatCatch(
() =>
this.queue.add("email-retry", {
to,
subject,
body,
attempts: 0,
lastError: error.message,
}),
errorTransformers.forLogging
);
if (queueError) {
console.error("Failed to queue email for retry:", queueError);
}
throw new Error(`Email delivery failed: ${error.message}`);
}
return result;
}
async processBatchEmails(
emails: Array<{ to: string; subject: string; body: string }>
) {
const results = await Promise.allSettled(
emails.map(async (email, index) => {
const [result, error] = await neatCatch(
() => this.sendEmail(email.to, email.subject, email.body),
errorTransformers.withContext({ batchIndex: index })
);
return { index, email: email.to, result, error };
})
);
const successful = results
.filter((r) => r.status === "fulfilled" && !r.value.error)
.map((r) => (r.status === "fulfilled" ? r.value : null))
.filter(Boolean);
const failed = results
.filter(
(r) =>
r.status === "rejected" || (r.status === "fulfilled" && r.value.error)
)
.map((r) =>
r.status === "fulfilled" ? r.value : { error: r.reason?.message }
);
return {
successful: successful.length,
failed: failed.length,
details: { successful, failed },
};
}
}
// WebSocket connection with reconnection
class WebSocketService {
private ws: WebSocket | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
async connect(url: string) {
const [, error] = await neatCatchRetry(
() =>
new Promise<void>((resolve, reject) => {
const ws = new WebSocket(url);
ws.onopen = () => {
this.ws = ws;
this.reconnectAttempts = 0;
resolve();
};
ws.onerror = (error) => reject(error);
ws.onclose = () => this.handleReconnect(url);
}),
{
maxRetries: this.maxReconnectAttempts,
delay: 1000,
backoff: "exponential",
errorTransformer: errorTransformers.withContext({
operation: "websocket-connect",
url,
}),
}
);
if (error) {
throw new Error(`WebSocket connection failed: ${error.message}`);
}
}
private async handleReconnect(url: string) {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error("Max reconnection attempts reached");
return;
}
this.reconnectAttempts++;
const [, error] = await neatCatch(
() =>
new Promise((resolve) =>
setTimeout(resolve, 1000 * this.reconnectAttempts)
),
errorTransformers.toString
);
if (!error) {
await this.connect(url);
}
}
async sendMessage(data: any) {
const [, error] = await neatCatch(
() => {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
throw new Error("WebSocket not connected");
}
this.ws.send(JSON.stringify(data));
},
errorTransformers.withContext({
operation: "websocket-send",
dataType: typeof data,
})
);
if (error) {
throw new Error(`Failed to send message: ${error.error}`);
}
}
}
```
```typescript
import { neatCatch, createNeatWrapper } from "neat-catch";
const safeValidate = createNeatWrapper((data: any) => {
if (!data.email) throw new Error("Email is required");
if (!data.password) throw new Error("Password is required");
return { valid: true };
});
function handleFormSubmit(formData: any) {
const [validation, error] = safeValidate(formData);
if (error) {
showErrorMessage(error.message);
return;
}
// Continue with valid form data
submitForm(formData);
}
```
```typescript
import { neatCatch } from "neat-catch";
import { readFile, writeFile } from "fs/promises";
async function processFile(inputPath: string, outputPath: string) {
const [content, readError] = await neatCatch(() =>
readFile(inputPath, "utf8")
);
if (readError) {
console.error(`Failed to read ${inputPath}:`, readError.message);
return false;
}
const [processed, processError] = neatCatch(
() => content.toUpperCase() // Some processing
);
if (processError) {
console.error("Processing failed:", processError.message);
return false;
}
const [, writeError] = await neatCatch(() =>
writeFile(outputPath, processed)
);
if (writeError) {
console.error(`Failed to write ${outputPath}:`, writeError.message);
return false;
}
return true;
}
```
```typescript
async function traditionalWay() {
let userData;
let userPosts;
try {
const userResponse = await fetch("/api/user");
userData = await userResponse.json();
const postsResponse = await fetch(`/api/posts/${userData.id}`);
userPosts = await postsResponse.json();
return { user: userData, posts: userPosts };
} catch (error) {
console.error("Server error:", error);
return;
}
}
```
```typescript
async function neatWay() {
const [userData, userError] = await neatCatch(async () => {
const response = await fetch("/api/user");
return response.json();
});
if (userError) {
console.error("Failed to fetch user:", userError);
return;
}
const [userPosts, postsError] = await neatCatch(async () => {
const response = await fetch(`/api/posts/${userData.id}`);
return response.json();
});
if (postsError) {
console.error("Failed to fetch posts:", postsError);
return;
}
return { user: userData, posts: userPosts };
}
```
As you can see this is written in a synchronous way which makes it easier to follow and it encourages handle of errors. This makes debugging much easier as well.
neat-catch provides excellent TypeScript support with intelligent type inference:
```typescript
// Types are automatically inferred
const [stringResult, error1] = neatCatch(() => "hello");
// stringResult: string | null, error1: Error | null
const [numberResult, error2] = await neatCatch(async () => 42);
// numberResult: number | null, error2: Error | null
// Custom error types
const [data, customError] = neatCatch(
() => riskyOperation(),
(err): CustomError => ({ code: 500, message: String(err) })
);
// customError: CustomError | null
```
MIT Ā© [dforrunner](https://github.com/dforrunner/neat-catch/blob/HEAD/LICENSE)
![Status](https: