
Research
Supply Chain Attack on Axios Pulls Malicious Dependency from npm
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.
A comprehensive utility library for elegant error handling in TypeScript and JavaScript. Handle both sync and async operations with clean tuple-based results.
A comprehensive utility library for elegant error handling in TypeScript and JavaScript. Transform messy try-catch blocks into clean, readable tuple-based results.
[data, error] tuplespnpm install neat-catch
# or
npm install neat-catch
# or
yarn add neat-catch
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
}
neatCatch<TFn, E>(fn, errorTransformer?)fn: TFn - The sync or async function to executeerrorTransformer?: (error: unknown) => E - Optional function to transform caught errors[ReturnType<TFn>, null] | [null, E]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).
// 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}`
);
createNeatWrapper<TArgs, TReturn, E>(fn, errorTransformer?)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<TOverride = TReturn>(...args: TArgs) => [TOverride, null] | [null, E]<TOverride = TReturn>(...args: TArgs) => Promise<[TOverride, null] | [null, E]>Creates a wrapped version of a function that always returns a neat tuple when called.
// 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]
neatCatchAll<T, E>(operations, errorTransformer?)operations: { [K in keyof T]: () => Promise<T[K]> } - An array of functions returning PromiseserrorTransformer?: (error: unknown) => E - Optional function to transform caught errorsPromise<{
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.
// 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}`
);
neatCatchRetry<T, E>(fn, options?)fn: () => Promise<T> - The async function to executeoptions?: NeatCatchRetryOptions<E> - Configuration options for retriestype 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.
// 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,
}),
});
errorTransformers.toStringerror: unknown - The error to transformstring - Error message as string
Transforms any error into a simple message string.
const [data, error] = neatCatch((): void => {
throw new Error("Something went wrong");
}, errorTransformers.toString);
// error will be: "Something went wrong"
errorTransformers.toObjecterror: unknown - The error to transform{
message: string;
stack?: string;
name?: string;
cause?: unknown;
}
Transforms errors into a structured object with message and stack information.
const [data, error] = neatCatch((): void => {
throw new TypeError("Invalid type");
}, errorTransformers.toObject);
// error will be: { message: "Invalid type", stack: "...", name: "TypeError" }
errorTransformers.withTimestamperror: unknown - The error to transform{
error: unknown;
timestamp: number;
isoString: string;
}
Transforms errors into a structured object with timestamp information.
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" }
errorTransformers.fetchErrorerror: unknown - The error to transform{
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.
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");
}
}
errorTransformers.withContextcontext: T - Additional context to include with the errorFunction that transforms error: (error: unknown) => T & { error: unknown }
Wraps an error with additional context information.
const [data, error] = neatCatch(
() => processUserData(userId),
errorTransformers.withContext({
userId,
operation: "processUserData",
timestamp: Date.now(),
})
);
// error will include the context along with the original error
errorTransformers.toSimpleErrorerror: unknown - The error to transform{ message: string }
Simplifies errors by extracting only the message and discarding other properties.
const [data, error] = neatCatch(() => {
throw new Error("Simple error");
}, errorTransformers.toSimpleError);
// error will be: { message: "Simple error" }
errorTransformers.forLoggingerror: unknown - The error to transform{
message: string;
stack?: string;
name?: string;
timestamp: number;
environment?: string;
}
Transforms errors for logging purposes with detailed information including environment context.
const [data, error] = neatCatch((): void => {
throw new Error("Logging error");
}, errorTransformers.forLogging);
// error will include message, stack, timestamp, and NODE_ENV if available
NeatCatchResult<T, E>type NeatCatchResult<T, E = Error> = [T, null] | [null, E];
Type utility representing the neat tuple result pattern used throughout the library.
function processData(): NeatCatchResult<string> {
try {
return ["processed data", null];
} catch (error) {
return [null, error as Error];
}
}
NeatCatchRetryOptions<E>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.
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);
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");
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]);
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 npm package. It works similar to neat-catch but it's specifically geared towards fetch.
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;
}
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}`);
}
}
}
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);
}
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;
}
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;
}
}
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:
// 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
FAQs
A comprehensive utility library for elegant error handling in TypeScript and JavaScript. Handle both sync and async operations with clean tuple-based results.
We found that neat-catch demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.

Research
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.

Security News
TeamPCP is partnering with ransomware group Vect to turn open source supply chain attacks on tools like Trivy and LiteLLM into large-scale ransomware operations.