# Skill: Error Handling & Logging

## Identity
- **Skill ID**: `error-handling-logging`
- **Domain**: Error Management, Structured Logging, Monitoring
- **Technologies**: Winston, Error Classes, Audit Logging
- **Source Agent**: `backend-app-expert.md`

## When to Load This Skill

Load this skill when working on:
- Winston logger configuration
- Custom error classes
- Error middleware
- Audit log implementation
- Monitoring and alerting
- Debug logging
- Production error tracking

**File patterns:**
- `api/src/middleware/error*.ts`
- `api/src/utils/logger*.ts`
- `api/src/utils/errors*.ts`

## Core Patterns

### 1. Winston Logger Setup

```typescript
// utils/logger.ts
import winston from 'winston';
import path from 'path';

// Custom log format
const logFormat = winston.format.combine(
  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
  winston.format.errors({ stack: true }),
  winston.format.splat(),
  winston.format.json()
);

// Console format (development)
const consoleFormat = winston.format.combine(
  winston.format.colorize(),
  winston.format.timestamp({ format: 'HH:mm:ss' }),
  winston.format.printf(({ timestamp, level, message, ...meta }) => {
    let msg = `${timestamp} [${level}]: ${message}`;
    if (Object.keys(meta).length > 0) {
      msg += ` ${JSON.stringify(meta)}`;
    }
    return msg;
  })
);

// Create logger instance
export const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: logFormat,
  defaultMeta: { service: 'translate-press-zone-api' },
  transports: [
    // Error logs
    new winston.transports.File({
      filename: path.join('logs', 'error.log'),
      level: 'error',
      maxsize: 5242880, // 5MB
      maxFiles: 5
    }),
    
    // Combined logs
    new winston.transports.File({
      filename: path.join('logs', 'combined.log'),
      maxsize: 5242880,
      maxFiles: 5
    })
  ]
});

// Console transport for development
if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: consoleFormat
  }));
}

// Stream for Morgan HTTP logging
export const httpLogStream = {
  write: (message: string) => {
    logger.http(message.trim());
  }
};
```

### 2. Custom Error Classes

```typescript
// utils/errors.ts

// Base API Error
export class ApiError extends Error {
  constructor(
    public statusCode: number,
    message: string,
    public isOperational: boolean = true
  ) {
    super(message);
    this.name = this.constructor.name;
    Error.captureStackTrace(this, this.constructor);
  }
}

// Specific error types
export class BadRequestError extends ApiError {
  constructor(message: string = 'Bad request') {
    super(400, message);
  }
}

export class UnauthorizedError extends ApiError {
  constructor(message: string = 'Unauthorized') {
    super(401, message);
  }
}

export class ForbiddenError extends ApiError {
  constructor(message: string = 'Forbidden') {
    super(403, message);
  }
}

export class NotFoundError extends ApiError {
  constructor(message: string = 'Resource not found') {
    super(404, message);
  }
}

export class ConflictError extends ApiError {
  constructor(message: string = 'Resource conflict') {
    super(409, message);
  }
}

export class PaymentRequiredError extends ApiError {
  constructor(message: string = 'Insufficient credits') {
    super(402, message);
  }
}

export class TooManyRequestsError extends ApiError {
  constructor(message: string = 'Rate limit exceeded') {
    super(429, message);
  }
}

export class InternalServerError extends ApiError {
  constructor(message: string = 'Internal server error') {
    super(500, message, false); // Not operational
  }
}

// Validation error with field details
export class ValidationError extends ApiError {
  constructor(
    public fields: Record<string, string>,
    message: string = 'Validation failed'
  ) {
    super(400, message);
  }
}
```

### 3. Error Handling Middleware

```typescript
// middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
import { ApiError } from '../utils/errors';
import { logger } from '../utils/logger';

export function errorHandler(
  err: Error,
  req: Request,
  res: Response,
  next: NextFunction
) {
  // Default to 500 server error
  let statusCode = 500;
  let message = 'Internal server error';
  let isOperational = false;
  
  if (err instanceof ApiError) {
    statusCode = err.statusCode;
    message = err.message;
    isOperational = err.isOperational;
  }
  
  // Log error
  const logData = {
    error: {
      message: err.message,
      stack: err.stack,
      statusCode
    },
    request: {
      method: req.method,
      url: req.url,
      ip: req.ip,
      userId: (req as any).user?.userId
    }
  };
  
  if (isOperational) {
    logger.warn('Operational error', logData);
  } else {
    logger.error('Non-operational error', logData);
    
    // In production, don't expose internal errors
    if (process.env.NODE_ENV === 'production') {
      message = 'An unexpected error occurred';
    }
  }
  
  // Send error response
  res.status(statusCode).json({
    error: message,
    ...(err instanceof ValidationError && { fields: err.fields }),
    ...(process.env.NODE_ENV !== 'production' && { stack: err.stack })
  });
}

// 404 handler
export function notFoundHandler(req: Request, res: Response) {
  res.status(404).json({
    error: 'Endpoint not found',
    path: req.path
  });
}

// Async error wrapper
export function asyncHandler(
  fn: (req: Request, res: Response, next: NextFunction) => Promise<any>
) {
  return (req: Request, res: Response, next: NextFunction) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}
```

### 4. HTTP Request Logging

```typescript
// middleware/httpLogger.ts
import morgan from 'morgan';
import { httpLogStream } from '../utils/logger';

// Custom Morgan format
morgan.token('user-id', (req: any) => req.user?.userId || 'anonymous');

export const httpLogger = morgan(
  ':method :url :status :response-time ms - :user-id',
  { stream: httpLogStream }
);

// Skip logging health checks
export const httpLoggerWithSkip = morgan(
  ':method :url :status :response-time ms - :user-id',
  {
    stream: httpLogStream,
    skip: (req) => req.url === '/health'
  }
);
```

### 5. Audit Logging

```typescript
// utils/auditLog.ts
import { prisma } from '../lib/prisma';
import { Request } from 'express';

interface AuditLogData {
  userId?: string;
  action: string;
  resourceType?: string;
  resourceId?: string;
  details?: any;
  req: Request;
}

export async function createAuditLog(data: AuditLogData): Promise<void> {
  try {
    await prisma.auditLog.create({
      data: {
        user_id: data.userId || null,
        action: data.action,
        resource_type: data.resourceType || null,
        resource_id: data.resourceId || null,
        ip_address: data.req.ip,
        user_agent: data.req.headers['user-agent'] || null,
        details: data.details || null
      }
    });
  } catch (error) {
    // Don't let audit log failure break the request
    logger.error('Failed to create audit log', { error, data });
  }
}

// Middleware to automatically audit specific actions
export function auditAction(action: string, resourceType?: string) {
  return async (req: any, res: Response, next: NextFunction) => {
    // Store original send function
    const originalSend = res.send;
    
    // Override send to capture success
    res.send = function (data: any) {
      if (res.statusCode >= 200 && res.statusCode < 300) {
        createAuditLog({
          userId: req.user?.userId,
          action,
          resourceType,
          resourceId: req.params.id,
          details: { method: req.method, body: req.body },
          req
        });
      }
      
      return originalSend.call(this, data);
    };
    
    next();
  };
}
```

### 6. Usage in Routes

```typescript
// Example: User management endpoint
import { asyncHandler } from '../middleware/errorHandler';
import { NotFoundError, BadRequestError } from '../utils/errors';
import { createAuditLog } from '../utils/auditLog';
import { logger } from '../utils/logger';

router.get('/users/:id', requireAuth, asyncHandler(async (req, res) => {
  const { id } = req.params;
  
  logger.info('Fetching user', { userId: id });
  
  const user = await prisma.user.findUnique({
    where: { id }
  });
  
  if (!user) {
    throw new NotFoundError('User not found');
  }
  
  res.json(user);
}));

router.put('/users/:id', requireAuth, asyncHandler(async (req, res) => {
  const { id } = req.params;
  const { status } = req.body;
  
  if (!['active', 'suspended'].includes(status)) {
    throw new BadRequestError('Invalid status value');
  }
  
  const user = await prisma.user.update({
    where: { id },
    data: { status }
  });
  
  // Audit log
  await createAuditLog({
    userId: (req as any).user.userId,
    action: 'user.update',
    resourceType: 'user',
    resourceId: id,
    details: { status },
    req
  });
  
  logger.info('User updated', { userId: id, status });
  
  res.json(user);
}));
```

### 7. Application Setup

```typescript
// app.ts
import express from 'express';
import { httpLoggerWithSkip } from './middleware/httpLogger';
import { errorHandler, notFoundHandler } from './middleware/errorHandler';
import { logger } from './utils/logger';

const app = express();

// Middleware
app.use(express.json());
app.use(httpLoggerWithSkip);

// Routes
app.use('/api', apiRoutes);

// Health check
app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

// 404 handler
app.use(notFoundHandler);

// Error handler (must be last)
app.use(errorHandler);

// Graceful shutdown
process.on('SIGTERM', () => {
  logger.info('SIGTERM received, shutting down gracefully');
  server.close(() => {
    logger.info('Server closed');
    process.exit(0);
  });
});

// Unhandled rejection
process.on('unhandledRejection', (reason, promise) => {
  logger.error('Unhandled Rejection', { reason, promise });
  process.exit(1);
});

// Uncaught exception
process.on('uncaughtException', (error) => {
  logger.error('Uncaught Exception', { error });
  process.exit(1);
});

export default app;
```

## Anti-Patterns (Forbidden)

| ❌ Mistake | ✅ Fix |
|-----------|--------|
| Using `console.log()` in production | Use Winston logger |
| Not catching async errors | Wrap with `asyncHandler()` |
| Exposing error stack traces in production | Hide in production, show in development |
| Generic "Error occurred" messages | Provide specific, actionable error messages |
| Not logging request context | Include user ID, IP, request details |
| Throwing strings instead of Error objects | Always throw Error instances |
| Not distinguishing operational vs programmer errors | Use `isOperational` flag |
| Logging sensitive data (passwords, tokens) | Sanitize logs |
| Not setting log rotation | Use maxFiles and maxSize |
| Letting audit log failures break requests | Catch and log audit errors separately |

## Integration with Other Skills

**Often combined with:**
- All skills (logging is cross-cutting)
- `authentication-security` - Log auth failures
- `payment-integration` - Audit payment events
- `api-endpoint-creation` - Error responses

## Environment Variables Required

```bash
# Logging Configuration
LOG_LEVEL="info"  # debug, info, warn, error
NODE_ENV="production"  # affects error verbosity
```

## Quick Reference

### Log Levels

| Level | When to Use |
|-------|-------------|
| `error` | Application errors, exceptions |
| `warn` | Operational errors (bad requests, auth failures) |
| `info` | Important business events (user created, payment completed) |
| `http` | HTTP requests/responses |
| `debug` | Detailed debugging information |

### Common Log Patterns

```typescript
// Info log
logger.info('User logged in', { userId: user.id, email: user.email });

// Warning log
logger.warn('Invalid API key attempt', { key: keyPrefix, ip: req.ip });

// Error log
logger.error('Database connection failed', { error: err.message, stack: err.stack });

// Debug log
logger.debug('Processing translation job', { jobId, tokens: estimatedTokens });
```

## Validation Checklist

- [ ] Winston logger configured with file transports
- [ ] Log rotation enabled (maxFiles, maxSize)
- [ ] Custom error classes created
- [ ] Error handler middleware implemented
- [ ] `asyncHandler` wraps all async routes
- [ ] HTTP request logging enabled
- [ ] Audit log for sensitive actions
- [ ] No `console.log()` in production code
- [ ] Error messages user-friendly
- [ ] Stack traces hidden in production
- [ ] Sensitive data (passwords, tokens) not logged
- [ ] Unhandled rejection handler registered
- [ ] Graceful shutdown implemented
- [ ] Log aggregation setup (optional: ELK, CloudWatch)

## Production Monitoring

### Recommended Services

- **Error Tracking**: Sentry, Rollbar
- **Log Aggregation**: ELK Stack, CloudWatch, Datadog
- **APM**: New Relic, AppDynamics

### Sentry Integration

```typescript
import * as Sentry from '@sentry/node';

if (process.env.NODE_ENV === 'production') {
  Sentry.init({
    dsn: process.env.SENTRY_DSN,
    environment: process.env.NODE_ENV,
    tracesSampleRate: 0.1
  });
  
  app.use(Sentry.Handlers.requestHandler());
  app.use(Sentry.Handlers.errorHandler());
}
```
