308 lines
6.7 KiB
Markdown
308 lines
6.7 KiB
Markdown
# 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<any>
|
|
) {
|
|
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)
|