Express.js’te Error Handling’i Profesyonelce Nasıl Yapıyorum?
Express.js ve Node.js projelerinde operational error ve system error ayrımını neden yaptığımı, AppError factory pattern ile hata yönetimini nasıl daha tutarlı, güvenli ve okunabilir hale getirdiğimi örnek kodlarla anlatıyorum.

Express.js’te Error Handling’i Profesyonelce Nasıl Yapıyorum?
Node.js backend geliştirirken en çok fark yaratan alanlardan biri bence error handling tarafı.
İlk zamanlarda ben de çoğu projede gördüğüm klasik yapıyı kullanıyordum:
throw new Error("Kullanıcı bulunamadı");
İlk bakışta bunda yanlış bir şey yok gibi görünüyor. Hata fırlatılıyor, middleware yakalıyor, response dönülüyor.
Ama proje büyüdükçe şu sorular ortaya çıkmaya başlıyor:
Bu hata kullanıcıya olduğu gibi gösterilmeli mi?
Bunun HTTP status code’u ne olmalı?
Bu beklenen bir iş kuralı hatası mı, yoksa sistemsel bir problem mi?
Hangi hata loglanmalı, hangisi kullanıcıya açıklanmalı?
Tüm response’ları nasıl tutarlı hale getireceğim?
İşte tam burada benim için en faydalı ayrım şu oldu: Her hata aynı değildir.
Operational Error vs System Error

Ben backend tarafında hataları kabaca ikiye ayırıyorum:
1. Operational Error
Bunlar sistemin beklediği, öngörülebilir ve kontrol edilebilir hatalar.
Örnekler:
Kullanıcı bulunamadı
Şifre yanlış
Token süresi dolmuş
Plan limiti aşıldı
Yetkisiz erişim denemesi
Doğrulama hatası
Yani sistem aslında çalışıyordur ama gelen istek iş kuralına uymuyordur. Bu tip hatalarda kullanıcıya anlamlı bir mesaj dönebilirim.
2. System Error (Non-Operational Error)
Bunlar beklenmeyen sistemsel problemlerdir.
Örnekler:
Database bağlantısı koptu
Üçüncü parti servis hata verdi
Kod içinde beklenmeyen bir exception oluştu
Null / undefined kaynaklı patlama
Dosya sistemi hatası
Timeout veya memory problemi
Bu tip hatalarda kullanıcıya iç detay göstermem. Bunlar loglanmalı, izlenmeli ve teknik olarak incelenmelidir.
Neden Bu Ayrım Önemli?
Bu ayrımı yapmadığında backend tarafında birkaç problem başlıyor:
1. Kullanıcıya gereksiz teknik hata gösterme riski oluşuyor
Kullanıcı yanlış şifre girdiyse ona stack trace dönmenin hiçbir anlamı yok.
2. Tüm hatalar aynı response formatında dönüyor
404 ile 500 birbirine karışıyor. İş kuralı hatası ile sistem hatası aynı görünmeye başlıyor.
3. Log tarafı kirleniyor
Beklenen hatalar ile kritik sistem hataları aynı yerde görününce gerçekten önemli olanı seçmek zorlaşıyor.
4. Kod içinde magic string çoğalıyor
Her yerde farklı farklı hata mesajları ve status code’lar oluşuyor. Proje büyüdükçe error handling tarafı kontrolü zor bir alana dönüşüyor.
Benim Kullandığım Yaklaşım: AppError Factory Pattern
Ben bunun için fonksiyon tabanlı bir AppError yapısı kullanıyorum. Temel mantık şu:
Her hata bir tip taşıyor
HTTP status code belli oluyor
isOperationalbilgisi taşınıyorHata tipi tek yerde tanımlanıyor
Kod içinde
throw new Error(...)yerine anlamlı factory fonksiyonlar kullanılıyor
Tip Tanımı
type AppError = Error & {
statusCode: number;
isOperational: boolean;
code?: string;
};
createAppError — Temel Fonksiyon
function createAppError(
message: string,
statusCode = 500,
isOperational = true,
code?: string
): AppError {
const error = new Error(message) as AppError;
error.name = "AppError";
error.statusCode = statusCode;
error.isOperational = isOperational;
error.code = code;
Error.captureStackTrace?.(error, createAppError);
return error;
}
Factory Fonksiyonlar
export function userNotFound(message = "Kullanıcı bulunamadı") {
return createAppError(message, 404, true, "USER_NOT_FOUND");
}
export function tokenExpired(message = "Oturum süresi doldu") {
return createAppError(message, 401, true, "TOKEN_EXPIRED");
}
export function unauthorized(message = "Bu işlem için yetkiniz yok") {
return createAppError(message, 403, true, "UNAUTHORIZED");
}
export function validation(message = "Geçersiz veri gönderildi") {
return createAppError(message, 400, true, "VALIDATION_ERROR");
}
export function planLimitReached(message = "Plan limitine ulaşıldı") {
return createAppError(message, 403, true, "PLAN_LIMIT_REACHED");
}
export function databaseError(message = "Veritabanı hatası") {
return createAppError(message, 500, false, "DATABASE_ERROR");
}
export function internalError(message = "Beklenmeyen bir hata oluştu") {
return createAppError(message, 500, false, "INTERNAL_SERVER_ERROR");
}
Kullanım Şekli
Şöyle bir şey yazmak yerine:
throw new Error("Kullanıcı bulunamadı");
Şöyle yazıyorum:
import * as AppError from "../errors/appError";
throw AppError.userNotFound();
// veya özel mesajla:
throw AppError.planLimitReached("Mevcut planınız yeni danışan eklemeye izin vermiyor");
import * as AppError sayesinde kullanım yeri açısından class versiyonuyla neredeyse birebir aynı hissettiriyor. Ama altta sade fonksiyonlar var.
Express Error Middleware

Bu yapının asıl gücü error middleware tarafında ortaya çıkıyor.
import { Request, Response, NextFunction } from "express";
import { AppError } from "../errors/appError";
import * as AppError from "../errors/appError";
function isAppError(err: unknown): err is AppError {
return err instanceof Error && "isOperational" in err;
}
export function errorHandler(
err: unknown,
req: Request,
res: Response,
_next: NextFunction
) {
const error = isAppError(err)
? err
: AppError.internalError("Beklenmeyen bir sunucu hatası oluştu");
if (error.isOperational) {
return res.status(error.statusCode).json({
success: false,
error: {
code: error.code,
message: error.message,
},
});
}
console.error("SYSTEM ERROR:", err);
return res.status(500).json({
success: false,
error: {
code: "INTERNAL_SERVER_ERROR",
message: "Sunucuda beklenmeyen bir hata oluştu",
},
});
}
Bu middleware ile temel karar mekanizması şu oluyor:
Operational error → kullanıcıya anlamlı mesaj
System error → log + generic response
Async Route’larda Kullanım
Express’te async fonksiyonlarda hata yönetimi dağılmaya çok müsait. Ben bunu toparlamak için küçük bir asyncHandler yardımcı fonksiyonu kullanıyorum.
import { Request, Response, NextFunction } from "express";
export const asyncHandler =
(
fn: (req: Request, res: Response, next: NextFunction) => Promise<unknown>
) =>
(req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
Kullanımı:
router.get(
"/users/:id",
asyncHandler(async (req, res) => {
const user = await userService.getById(req.params.id);
if (!user) {
throw AppError.userNotFound();
}
res.json({
success: true,
data: user,
});
})
);
Bu yapı sayesinde her route içinde ayrı ayrı try/catch yazmam gerekmiyor.
Validation Hataları Nasıl Yönetilebilir?
Zod veya benzeri bir yapı kullanıyorsam, validation hatalarını da aynı formata çeviriyorum.
import { z } from "zod";
import * as AppError from "../errors/appError";
const createUserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
});
export function validateCreateUser(body: unknown) {
const result = createUserSchema.safeParse(body);
if (!result.success) {
const message = result.error.issues.map(i => i.message).join(", ");
throw AppError.validation(message);
}
return result.data;
}
Neden Factory Pattern Kullanıyorum?
Bu yapının bana sağladığı en büyük avantajlar:
Magic string’ler azaldı — Her yerde “Kullanıcı bulunamadı” yazmıyorum, tek merkezden yönetiyorum.
HTTP status code dağılmadı — 404, 401, 403, 500 gibi kodlar kontrolsüz şekilde her dosyaya yayılmıyor.
Response formatı tutarlı kaldı — Frontend tarafı ne bekleyeceğini biliyor.
Loglama ve monitoring kolaylaştı — Hangi hata operational, hangisi kritik sistem hatası hemen ayırt edilebiliyor.
Ekip içinde standart oluştu — Bir hata fırlatılacaksa bunun nasıl yapılacağı belli oluyor.
Kısa Bir Örnek Akış
router.get(
"/me",
asyncHandler(async (req, res) => {
const userId = req.user?.id;
if (!userId) {
throw AppError.unauthorized("Oturum bilgisi bulunamadı");
}
const user = await userService.getById(userId);
if (!user) {
throw AppError.userNotFound();
}
res.json({
success: true,
data: user,
});
})
);
Olası davranışlar:
DurumSonuçKullanıcı giriş yapmamış401 / 403 anlamlı mesajKullanıcı bulunamadı404 anlamlı mesajDatabase patladı500 generic response + sistem logu
Dikkat Ettiğim Bir Nokta
Burada önemli olan şey her hatayı operational yapmak değil.
Örneğin database bağlantısı koptuysa bunu kullanıcıya "Veritabanı bağlantısı ECONNREFUSED verdi" şeklinde dönmek istemem. Bu bilgi geliştirici içindir, kullanıcı için değil.
Yani iyi error handling biraz da şu disiplini gerektiriyor: Kullanıcının bilmesi gereken ile sistemin bilmesi gereken şeyi ayırmak.
Sonuç
Backend tarafında hata yönetimi genelde ikinci plana atılıyor. Ama proje büyüdükçe bunun aslında mimarinin önemli bir parçası olduğu çok net görülüyor.
Benim için en kritik fikir şu oldu: Tüm hatalar aynı değildir.
Beklenen iş kuralı hatalarını kullanıcıya doğru şekilde göstermek, beklenmeyen sistem hatalarını ise güvenli şekilde loglamak gerekir.
İyi bir error handling yapısı:
Kullanıcı deneyimini iyileştirir
Backend kodunu standartlaştırır
Debug süresini azaltır
Sistemi daha profesyonel hale getirir
Ve çoğu zaman farkı yaratan şey çok büyük bir teknoloji değişimi değil, böyle küçük ama doğru pattern’ler oluyor.
Kısa Özet:
Her hata aynı değildir
Operational ve system error ayrımı önemlidir
Fonksiyon tabanlı factory pattern kodu temizler
Middleware tarafında tek bir kontrol büyük fark yaratır
Kullanıcıya her zaman sadece bilmesi gereken bilgi dönülmelidir
Error handling, backend tarafında profesyonellik göstergesidir
Summary
This article explains how to handle errors professionally in Node.js and Express.js backends by separating Operational Errors (expected business logic failures like “user not found” or “token expired”) from System Errors (unexpected failures like database crashes or third-party service outages). It introduces a function-based AppError factory pattern where a core
createAppErrorfunction and named factory functions (userNotFound,unauthorized,planLimitReached, etc.) replace genericthrow new Error(...)calls throughout the codebase. The pattern is tied together with a centralized Express error middleware that returns meaningful responses to users for operational errors while safely logging system errors without exposing internal details. Supporting utilities likeasyncHandlerand Zod-based validation wrappers are also covered to complete the approach.
Daha yeni
Timeout Neden Hayati? Cevap Beklemek Bedava Değil
Daha eski