Pinned
Refactoring async TypeScript patterns
You could use Promise.allSettled for better error handling...
Today
Marketing copy for SaaS landing page
Here's the revised headline with a stronger value prop...
Explaining transformer architecture
Self-attention allows each token to attend to every other...
SQL query optimization strategies
Adding a composite index on (user_id, created_at) should...
React Server Components deep dive
RSCs run exclusively on the server and can directly access...
Yesterday
Investment thesis for AI infrastructure
The picks-and-shovels play here is GPU cloud providers...
Designing a rate limiter in Redis
The sliding window counter approach gives you the most...
This Week
Python data pipeline with Polars
Polars' lazy evaluation lets you chain operations and...
Onboarding email sequence copy
Day 3 email should focus on the first "aha moment"...
Kubernetes pod autoscaling config
Set targetCPUUtilizationPercentage to 70 and minReplicas...
Writing a technical spec for auth system
The spec should cover token refresh flow, session expiry...
Debugging memory leak in Node.js
Use --expose-gc and track heap snapshots over time to...
Marcus Chen
Pro Plan · 48k tokens left
I have a TypeScript codebase with a lot of async functions that use try/catch blocks everywhere. What are the best patterns to handle errors more elegantly?
Great question! There are several elegant patterns for async error handling in TypeScript. Let me walk through the most effective ones:
The most robust approach is using a discriminated union type:
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
async function fetchUser(id: string): Promise<Result<User>> {
try {
const user = await db.users.findById(id);
return { success: true, data: user };
} catch (err) {
return { success: false, error: err as Error };
}
}
// Usage — no try/catch at the call site
const result = await fetchUser('user-123');
if (result.success) {
console.log(result.data.name);
} else {
console.error(result.error.message);
}When running multiple async operations:
const [usersResult, postsResult] = await Promise.allSettled([
fetchUsers(),
fetchPosts(),
]);
if (usersResult.status === 'fulfilled') {
processUsers(usersResult.value);
}Create a higher-order function that wraps any async operation:
async function withErrorHandling<T>(
fn: () => Promise<T>,
fallback?: T
): Promise<T | undefined> {
try {
return await fn();
} catch (error) {
logger.error(error);
return fallback;
}
}Which pattern fits your codebase best depends on whether you need the error details downstream or just want to suppress them gracefully.
The Result type pattern looks really clean. How would I handle this in a Next.js API route that also needs to return proper HTTP status codes?
Perfect use case for the Result pattern in Next.js! Here's how to wire it up with proper HTTP semantics:
// lib/api-response.ts
import { NextResponse } from 'next/server';
type ApiError = {
code: string;
message: string;
details?: unknown;
};
export function apiSuccess<T>(data: T, status = 200) {
return NextResponse.json({ success: true, data }, { status });
}
export function apiError(error: ApiError, status = 500) {
return NextResponse.json({ success: false, error }, { status });
}
// app/api/users/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const result = await fetchUser(params.id);
if (!result.success) {
if (result.error instanceof NotFoundError) {
return apiError({ code: 'USER_NOT_FOUND', message: 'User does not exist' }, 404);
}
return apiError({ code: 'INTERNAL_ERROR', message: 'Failed to fetch user' }, 500);
}
return apiSuccess(result.data);
}You could also use a custom error class hierarchy to make the status code mapping automatic:
class AppError extends Error {
constructor(
public message: string,
public statusCode: number,
public code: string
) {
super(message);
}
}
class NotFoundError extends AppError {
constructor(resource: string) {
super(`${resource} not found`, 404, 'NOT_FOUND');
}
}This keeps your route handlers clean and ensures consistent error shapes across your entire API.
Could you also show me how to handle validation errors differently from runtime errors? I'm using Zod for schema validation.
Absolutely — Zod integrates beautifully with this pattern. The key is distinguishing validation errors (client mistakes, 400) from runtime errors (server problems, 500):
import { z } from 'zod';
const CreateUserSchema = z.object({
email: z.string().email('Invalid email format'),
name: z.string().min(2, 'Name must be at least 2 characters'),
role: z.enum(['admin', 'user', 'viewer']),
});
export async function POST(request: Request) {
const body = await request.json();
// Validate first — this is a client error
const parsed = CreateUserSchema.safeParse(body);
if (!parsed.success) {
return apiError({
code: 'VALIDATION_ERROR',
message: 'Invalid request body',
details: parsed.error.flatten().fieldErrors,
}, 400);
}
// Now handle runtime errors separately
const result = await createUser(parsed.data);
if (!result.success) {
return apiError({ code: 'CREATE_FAILED', message: result.error.message }, 500);
}
return apiSuccess(result.data, 201);
}The flatten().fieldErrors gives you a clean object like:
{
"email": ["Invalid email format"],
"name": ["Name must be at least 2 characters"]
}Which your frontend can map directly to form field errors. Clean separation of concerns — Zod owns validation shape, your Result type owns runtime outcomes.
ChatAI can make mistakes. Verify important information.