Async/Await Error Handling Patterns That Don't Swallow Errors
Unhandled promise rejections crash Node.js processes. Empty catch blocks hide bugs for months. Here are the patterns that handle errors without swallowing them.
The Anti-Pattern
// WRONG: swallows the error silently
async function getUser(id: string) {
try {
return await db.user.findUnique({ where: { id } });
} catch (e) {
// Error disappears — you have no idea what went wrong
}
}
Pattern 1: Let It Throw
// Let errors propagate — handle at the boundary
async function getUser(id: string) {
return db.user.findUnique({ where: { id } });
// If this throws, the caller handles it
}
// Handle at the API route level
app.get('/users/:id', async (req, res, next) => {
try {
const user = await getUser(req.params.id);
res.json(user);
} catch (error) {
next(error); // Express error handler
}
});
Pattern 2: Result Type
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
async function getUser(id: string): Promise<Result<User>> {
try {
const user = await db.user.findUnique({ where: { id } });
if (!user) return { ok: false, error: new Error('Not found') };
return { ok: true, value: user };
} catch (e) {
return { ok: false, error: e as Error };
}
}
// Caller must handle both cases
const result = await getUser(id);
if (!result.ok) {
return res.status(404).json({ error: result.error.message });
}
const user = result.value; // TypeScript knows this is User
Pattern 3: to() Helper
// Inspired by Go's error handling
async function to<T>(promise: Promise<T>): Promise<[Error | null, T | null]> {
try {
return [null, await promise];
} catch (e) {
return [e as Error, null];
}
}
// Usage
const [err, user] = await to(getUser(id));
if (err) return res.status(500).json({ error: err.message });
// user is guaranteed non-null here
Pattern 4: Error Classes
class NotFoundError extends Error {
statusCode = 404;
constructor(resource: string, id: string) {
super(`${resource} ${id} not found`);
this.name = 'NotFoundError';
}
}
class ValidationError extends Error {
statusCode = 400;
constructor(public fields: Record<string, string>) {
super('Validation failed');
this.name = 'ValidationError';
}
}
// Central error handler
app.use((error: Error, req, res, next) => {
if (error instanceof NotFoundError) {
return res.status(404).json({ error: error.message });
}
if (error instanceof ValidationError) {
return res.status(400).json({ error: error.message, fields: error.fields });
}
// Unknown error
Sentry.captureException(error);
res.status(500).json({ error: 'Internal server error' });
});
Promise.allSettled vs Promise.all
// Promise.all — fails fast if ANY promise rejects
const [user, posts] = await Promise.all([getUser(id), getPosts(id)]);
// Promise.allSettled — runs all, returns status of each
const results = await Promise.allSettled([getUser(id), getPosts(id)]);
const user = results[0].status === 'fulfilled' ? results[0].value : null;
const posts = results[1].status === 'fulfilled' ? results[1].value : [];
Error handling patterns are used throughout the AI SaaS Starter Kit — typed error classes, central Express handler, Sentry integration. $99 at whoffagents.com.
Top comments (0)