İçeriğe geç
Node.js

Node.js Error Handling: Production-Ready Hata Yönetimi

Tarık Tunç
Node.js Error Handling: Production-Ready Hata Yönetimi

Node.js Error Handling: Production-Ready Hata Yönetimi

Hata yönetimi, production-ready bir Node.js uygulamasının en kritik bileşenlerinden biridir. Doğru hata yönetimi stratejisi, uygulamanızın güvenilirliğini artırır, debugging sürecini kolaylaştırır ve kullanıcı deneyimini iyileştirir. Bu rehberde Node.js'te hata türlerini, operational ve programming error ayrımını, Express.js'te merkezi hata yönetimini, async error handling pattern'lerini ve production ortamı için en iyi pratikleri kapsamlı şekilde inceleyeceğiz.

1. Node.js'te Hata Türleri

Node.js'te hatalar iki ana kategoriye ayrılır: Operational errors (beklenen, yönetilebilir hatalar) ve Programming errors (kod hataları, bug'lar). Bu ayrım, hata yönetimi stratejinizin temelini oluşturur. Operational error'lar (veritabanı bağlantı hatası, geçersiz kullanıcı girişi, timeout) graceful şekilde yönetilmelidir. Programming error'lar (undefined property erişimi, type error) ise genellikle uygulamanın yeniden başlatılmasını gerektirir.

// Özel hata sınıfları hiyerarşisi
class AppError extends Error {
  constructor(message, statusCode, errorCode) {
    super(message);
    this.name = this.constructor.name;
    this.statusCode = statusCode;
    this.errorCode = errorCode;
    this.isOperational = true;
    this.timestamp = new Date().toISOString();
    Error.captureStackTrace(this, this.constructor);
  }
}

class ValidationError extends AppError {
  constructor(errors) {
    super('Doğrulama hatası', 400, 'VALIDATION_ERROR');
    this.errors = errors;
  }
}

class NotFoundError extends AppError {
  constructor(resource = 'Kaynak') {
    super(`${resource} bulunamadı`, 404, 'NOT_FOUND');
  }
}

class AuthenticationError extends AppError {
  constructor(message = 'Kimlik doğrulama başarısız') {
    super(message, 401, 'AUTHENTICATION_ERROR');
  }
}

class AuthorizationError extends AppError {
  constructor(message = 'Bu işlem için yetkiniz yok') {
    super(message, 403, 'AUTHORIZATION_ERROR');
  }
}

class ConflictError extends AppError {
  constructor(message = 'Kaynak zaten mevcut') {
    super(message, 409, 'CONFLICT');
  }
}

class RateLimitError extends AppError {
  constructor() {
    super('Çok fazla istek, lütfen daha sonra tekrar deneyin', 429, 'RATE_LIMIT');
  }
}

module.exports = {
  AppError, ValidationError, NotFoundError,
  AuthenticationError, AuthorizationError,
  ConflictError, RateLimitError
};
Bilgi: Özel hata sınıfları oluşturmak, hataları kategorize etmenize, tutarlı hata yanıtları döndürmenize ve farklı hata türleri için farklı loglama stratejileri uygulamanıza olanak tanır. isOperational flag'i, hatanın beklenen bir durum mu yoksa beklenmeyen bir bug mı olduğunu ayırt etmenizi sağlar.

2. Express.js Merkezi Hata Yönetimi

Express.js'te tüm hataları tek bir noktada yakalamak, tutarlı hata yanıtları döndürmek ve hataları loglamak için global error handling middleware kullanılır. Bu middleware, uygulamanın en son middleware'i olarak tanımlanmalıdır.

// Async handler wrapper
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// Controller'da kullanım
const getUserById = asyncHandler(async (req, res) => {
  const user = await User.findById(req.params.id);
  if (!user) {
    throw new NotFoundError('Kullanıcı');
  }
  res.json({ success: true, data: user });
});

// Global Error Handler Middleware
const errorHandler = (err, req, res, next) => {
  // Varsayılan değerler
  let statusCode = err.statusCode || 500;
  let message = err.message || 'Sunucu hatası';
  let errorCode = err.errorCode || 'INTERNAL_ERROR';

  // Mongoose hataları
  if (err.name === 'CastError') {
    statusCode = 400;
    message = `Geçersiz ${err.path} değeri: ${err.value}`;
    errorCode = 'INVALID_ID';
  }

  if (err.code === 11000) {
    statusCode = 409;
    const field = Object.keys(err.keyValue)[0];
    message = `Bu ${field} değeri zaten kullanılıyor`;
    errorCode = 'DUPLICATE_KEY';
  }

  if (err.name === 'ValidationError' && err.errors) {
    statusCode = 400;
    message = Object.values(err.errors).map(e => e.message).join(', ');
    errorCode = 'VALIDATION_ERROR';
  }

  // JWT hataları
  if (err.name === 'JsonWebTokenError') {
    statusCode = 401;
    message = 'Geçersiz token';
    errorCode = 'INVALID_TOKEN';
  }

  if (err.name === 'TokenExpiredError') {
    statusCode = 401;
    message = 'Token süresi dolmuş';
    errorCode = 'TOKEN_EXPIRED';
  }

  // Loglama
  if (statusCode >= 500) {
    console.error(`[CRITICAL] ${err.message}`, {
      stack: err.stack,
      url: req.originalUrl,
      method: req.method,
      ip: req.ip,
      userId: req.user?.id
    });
  } else {
    console.warn(`[WARN] ${statusCode} - ${message}`, {
      url: req.originalUrl,
      method: req.method
    });
  }

  // Response
  res.status(statusCode).json({
    success: false,
    error: {
      message,
      code: errorCode,
      ...(process.env.NODE_ENV === 'development' && {
        stack: err.stack,
        details: err.errors
      })
    }
  });
};

// Express app'e ekle
app.use(errorHandler);
Uyarı: Production ortamında hata stack trace'lerini asla istemciye göndermeyin. Stack trace'ler, uygulamanızın iç yapısı hakkında saldırganlara bilgi verebilir. Stack trace'leri sadece sunucu loglarında saklayın.

3. Unhandled Error ve Process-Level Hata Yönetimi

Express middleware'inin yakayamadığı hatalar da vardır: unhandled promise rejection'lar ve uncaught exception'lar. Bu hataları process seviyesinde yakalamak, uygulamanın sessizce çökmesini önler ve graceful shutdown sağlar.

// Unhandled Promise Rejection
process.on('unhandledRejection', (reason, promise) => {
  console.error('[FATAL] Unhandled Promise Rejection:', {
    reason: reason?.message || reason,
    stack: reason?.stack
  });

  // Graceful shutdown
  server.close(() => {
    console.log('Sunucu kapatıldı (unhandled rejection)');
    process.exit(1);
  });

  // 10 saniye içinde kapanamadıysa zorla kapat
  setTimeout(() => {
    console.error('Zorla kapatılıyor...');
    process.exit(1);
  }, 10000);
});

// Uncaught Exception
process.on('uncaughtException', (error) => {
  console.error('[FATAL] Uncaught Exception:', {
    message: error.message,
    stack: error.stack
  });

  // Uncaught exception sonrası uygulama güvenilmez durumdadır
  // Mutlaka yeniden başlatılmalıdır
  process.exit(1);
});

// SIGTERM handler (Docker, Kubernetes graceful shutdown)
process.on('SIGTERM', () => {
  console.log('SIGTERM alındı. Graceful shutdown başlatılıyor...');
  server.close(() => {
    console.log('HTTP sunucusu kapatıldı');
    // Veritabanı bağlantılarını kapat
    mongoose.connection.close(false, () => {
      console.log('MongoDB bağlantısı kapatıldı');
      process.exit(0);
    });
  });
});

4. Retry Pattern ve Circuit Breaker

Dış servislere yapılan çağrılarda geçici hatalar (network timeout, 503 Service Unavailable) sıkça karşılaşılan durumlardır. Retry pattern, başarısız istekleri belirli aralıklarla tekrar dener. Circuit breaker ise ardışık hata sayısı bir eşiği aştığında istekleri geçici olarak durdurarak dış servisin toparlanmasına zaman tanır.

// Retry mekanizması (exponential backoff ile)
async function retryOperation(operation, options = {}) {
  const { maxRetries = 3, baseDelay = 1000, maxDelay = 10000 } = options;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      if (attempt === maxRetries) throw error;

      // Sadece geçici hatalar için tekrar dene
      if (!isRetryable(error)) throw error;

      const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
      const jitter = delay * (0.5 + Math.random() * 0.5);

      console.warn(`Deneme ${attempt}/${maxRetries} başarısız. ` +
        `${Math.round(jitter)}ms sonra tekrar denenecek...`);

      await new Promise(resolve => setTimeout(resolve, jitter));
    }
  }
}

function isRetryable(error) {
  const retryableErrors = ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED'];
  const retryableStatuses = [408, 429, 500, 502, 503, 504];

  return retryableErrors.includes(error.code) ||
    retryableStatuses.includes(error.statusCode);
}

// Kullanım
const data = await retryOperation(
  () => fetch('https://api.example.com/data').then(r => r.json()),
  { maxRetries: 3, baseDelay: 1000 }
);
İpucu: Retry mekanizmasında exponential backoff ve jitter kullanmak, tüm istemcilerin aynı anda tekrar denemesini (thundering herd) önler. Her tekrar denemesinde bekleme süresini artırarak sunucu üzerindeki yükü dengelersiniz.

5. Production Error Monitoring

Production ortamında hataları izlemek ve anında haberdar olmak için Sentry, Datadog veya New Relic gibi error monitoring servisleri kullanılmalıdır. Bu araçlar hata sıklığını, etkilenen kullanıcı sayısını, stack trace'leri ve hata trendlerini görselleştirir. Ayrıca alert mekanizmaları ile kritik hatalar anında ekibe bildirilir.

Structured logging ile hatalarınızı JSON formatında kaydedin. Bu sayede log aggregation araçları (ELK Stack, Grafana Loki) ile logları kolayca arayabilir, filtreleyebilir ve analiz edebilirsiniz. Her log kaydına request ID, kullanıcı bilgisi ve context eklemek, hata ayıklama sürecini büyük ölçüde hızlandırır.

Sonuç

Production-ready hata yönetimi, özel hata sınıfları, merkezi error handler, process-level hata yakalama, retry mekanizmaları ve monitoring araçlarının bir bütün olarak uygulanmasını gerektirir. Bu rehberdeki pattern'leri uygulamaya koyarak uygulamanızın güvenilirliğini artırabilir, hataları hızlıca tespit edebilir ve kullanıcılarınıza tutarlı bir deneyim sunabilirsiniz.

Kaynaklar

İlgili Yazılar