# Async Patterns and Error Handling Complete guide to async/await patterns and custom error handling. ## Table of Contents - [Async/Await Best Practices](#asyncawait-best-practices) - [Promise Error Handling](#promise-error-handling) - [Custom Error Types](#custom-error-types) - [asyncErrorWrapper Utility](#asyncerrorwrapper-utility) - [Error Propagation](#error-propagation) - [Common Async Pitfalls](#common-async-pitfalls) --- ## Async/Await Best Practices ### Always Use Try-Catch ```typescript // ❌ NEVER: Unhandled async errors async function fetchData() { const data = await database.query(); // If throws, unhandled! return data; } // ✅ ALWAYS: Wrap in try-catch async function fetchData() { try { const data = await database.query(); return data; } catch (error) { Sentry.captureException(error); throw error; } } ``` ### Avoid .then() Chains ```typescript // ❌ AVOID: Promise chains function processData() { return fetchData() .then(data => transform(data)) .then(transformed => save(transformed)) .catch(error => { console.error(error); }); } // ✅ PREFER: Async/await async function processData() { try { const data = await fetchData(); const transformed = await transform(data); return await save(transformed); } catch (error) { Sentry.captureException(error); throw error; } } ``` --- ## Promise Error Handling ### Parallel Operations ```typescript // ✅ Handle errors in Promise.all try { const [users, profiles, settings] = await Promise.all([ userService.getAll(), profileService.getAll(), settingsService.getAll(), ]); } catch (error) { // One failure fails all Sentry.captureException(error); throw error; } // ✅ Handle errors individually with Promise.allSettled const results = await Promise.allSettled([ userService.getAll(), profileService.getAll(), settingsService.getAll(), ]); results.forEach((result, index) => { if (result.status === 'rejected') { Sentry.captureException(result.reason, { tags: { operation: ['users', 'profiles', 'settings'][index] } }); } }); ``` --- ## Custom Error Types ### Define Custom Errors ```typescript // Base error class export class AppError extends Error { constructor( message: string, public code: string, public statusCode: number, public isOperational: boolean = true ) { super(message); this.name = this.constructor.name; Error.captureStackTrace(this, this.constructor); } } // Specific error types export class ValidationError extends AppError { constructor(message: string) { super(message, 'VALIDATION_ERROR', 400); } } export class NotFoundError extends AppError { constructor(message: string) { super(message, 'NOT_FOUND', 404); } } export class ForbiddenError extends AppError { constructor(message: string) { super(message, 'FORBIDDEN', 403); } } export class ConflictError extends AppError { constructor(message: string) { super(message, 'CONFLICT', 409); } } ``` ### Usage ```typescript // Throw specific errors if (!user) { throw new NotFoundError('User not found'); } if (user.age < 18) { throw new ValidationError('User must be 18+'); } // Error boundary handles them function errorBoundary(error, req, res, next) { if (error instanceof AppError) { return res.status(error.statusCode).json({ error: { message: error.message, code: error.code } }); } // Unknown error Sentry.captureException(error); res.status(500).json({ error: { message: 'Internal server error' } }); } ``` --- ## asyncErrorWrapper Utility ### Pattern ```typescript export function asyncErrorWrapper( handler: (req: Request, res: Response, next: NextFunction) => Promise ) { return async (req: Request, res: Response, next: NextFunction) => { try { await handler(req, res, next); } catch (error) { next(error); } }; } ``` ### Usage ```typescript // Without wrapper - error can be unhandled router.get('/users', async (req, res) => { const users = await userService.getAll(); // If throws, unhandled! res.json(users); }); // With wrapper - errors caught router.get('/users', asyncErrorWrapper(async (req, res) => { const users = await userService.getAll(); res.json(users); })); ``` --- ## Error Propagation ### Proper Error Chains ```typescript // ✅ Propagate errors up the stack async function repositoryMethod() { try { return await PrismaService.main.user.findMany(); } catch (error) { Sentry.captureException(error, { tags: { layer: 'repository' } }); throw error; // Propagate to service } } async function serviceMethod() { try { return await repositoryMethod(); } catch (error) { Sentry.captureException(error, { tags: { layer: 'service' } }); throw error; // Propagate to controller } } async function controllerMethod(req, res) { try { const result = await serviceMethod(); res.json(result); } catch (error) { this.handleError(error, res, 'controllerMethod'); // Final handler } } ``` --- ## Common Async Pitfalls ### Fire and Forget (Bad) ```typescript // ❌ NEVER: Fire and forget async function processRequest(req, res) { sendEmail(user.email); // Fires async, errors unhandled! res.json({ success: true }); } // ✅ ALWAYS: Await or handle async function processRequest(req, res) { try { await sendEmail(user.email); res.json({ success: true }); } catch (error) { Sentry.captureException(error); res.status(500).json({ error: 'Failed to send email' }); } } // ✅ OR: Intentional background task async function processRequest(req, res) { sendEmail(user.email).catch(error => { Sentry.captureException(error); }); res.json({ success: true }); } ``` ### Unhandled Rejections ```typescript // ✅ Global handler for unhandled rejections process.on('unhandledRejection', (reason, promise) => { Sentry.captureException(reason, { tags: { type: 'unhandled_rejection' } }); console.error('Unhandled Rejection:', reason); }); process.on('uncaughtException', (error) => { Sentry.captureException(error, { tags: { type: 'uncaught_exception' } }); console.error('Uncaught Exception:', error); process.exit(1); }); ``` --- **Related Files:** - [SKILL.md](SKILL.md) - [sentry-and-monitoring.md](sentry-and-monitoring.md) - [complete-examples.md](complete-examples.md)