TypeScript Error Handling: Tip Güvenli Hata Yönetimi

TypeScript Error Handling: Tip Güvenli Hata Yönetimi
Hata yönetimi, güvenilir uygulamaların temel taşıdır. TypeScript'in tip sistemi, hataları derleme zamanında yakalamamızı sağlar; ancak runtime hataları için de tip güvenli yaklaşımlar geliştirmek kritik önem taşır. Bu rehberde TypeScript'te hata yönetiminin en iyi pratiklerini, Result pattern'ı, custom error sınıflarını ve tip güvenli error handling stratejilerini inceleyeceğiz.
Try-Catch'in Tip Güvenliği Sorunu
JavaScript'in try-catch mekanizmasında yakalanan hata her zaman unknown tipindedir:
try {
const data = JSON.parse(userInput);
} catch (error) {
// error tipi: unknown (TypeScript 4.4+)
// TypeScript 4.0 öncesi: any
// Tip güvenli kullanım için kontrol gerekir
if (error instanceof SyntaxError) {
console.error('JSON parse hatası:', error.message);
} else if (error instanceof Error) {
console.error('Genel hata:', error.message);
} else {
console.error('Bilinmeyen hata:', String(error));
}
}
catch bloğundaki hata parametresi varsayılan olarak unknown tipindedir. Bu, tsconfig.json'da "useUnknownInCatchVariables": true (strict modda otomatik aktif) ile kontrol edilir.
Custom Error Sınıfları
Tip güvenli hata yönetiminin ilk adımı, özelleştirilmiş hata sınıfları oluşturmaktır:
// errors/base.ts
export class AppError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode: number = 500,
public readonly isOperational: boolean = true
) {
super(message);
this.name = this.constructor.name;
Object.setPrototypeOf(this, new.target.prototype);
}
}
export class ValidationError extends AppError {
constructor(
message: string,
public readonly fields: Record<string, string[]>
) {
super(message, 'VALIDATION_ERROR', 400);
}
}
export class NotFoundError extends AppError {
constructor(resource: string, id: string | number) {
super(`${resource} bulunamadı: ${id}`, 'NOT_FOUND', 404);
}
}
export class AuthenticationError extends AppError {
constructor(message = 'Kimlik doğrulama başarısız') {
super(message, 'AUTHENTICATION_ERROR', 401, true);
}
}
export class AuthorizationError extends AppError {
constructor(message = 'Bu işlem için yetkiniz yok') {
super(message, 'AUTHORIZATION_ERROR', 403, true);
}
}
Result Pattern (Either Monad)
Exception fırlatmak yerine, fonksiyonların başarı veya hata döndürmesini sağlayan Result pattern, fonksiyonel programlama yaklaşımıdır:
// lib/result.ts
type Success<T> = {
ok: true;
value: T;
};
type Failure<E> = {
ok: false;
error: E;
};
type Result<T, E = Error> = Success<T> | Failure<E>;
// Yardımcı fonksiyonlar
function ok<T>(value: T): Success<T> {
return { ok: true, value };
}
function err<E>(error: E): Failure<E> {
return { ok: false, error };
}
export { Result, ok, err };
Result Pattern Kullanımı
import { Result, ok, err } from './lib/result';
interface User {
id: string;
email: string;
name: string;
}
type CreateUserError =
| { type: 'VALIDATION'; fields: Record<string, string> }
| { type: 'DUPLICATE_EMAIL'; email: string }
| { type: 'DATABASE'; message: string };
async function createUser(
input: { email: string; name: string }
): Promise<Result<User, CreateUserError>> {
// Validasyon
if (!input.email.includes('@')) {
return err({
type: 'VALIDATION',
fields: { email: 'Geçerli bir email adresi giriniz' },
});
}
// Duplicate kontrol
const existing = await db.user.findByEmail(input.email);
if (existing) {
return err({
type: 'DUPLICATE_EMAIL',
email: input.email,
});
}
try {
const user = await db.user.create({ data: input });
return ok(user);
} catch (e) {
return err({
type: 'DATABASE',
message: e instanceof Error ? e.message : 'Bilinmeyen hata',
});
}
}
// Kullanım
async function handleCreateUser() {
const result = await createUser({
email: 'test@example.com',
name: 'Test',
});
if (!result.ok) {
switch (result.error.type) {
case 'VALIDATION':
console.log('Validasyon hatası:', result.error.fields);
break;
case 'DUPLICATE_EMAIL':
console.log('Bu email zaten kayıtlı:', result.error.email);
break;
case 'DATABASE':
console.log('Veritabanı hatası:', result.error.message);
break;
}
return;
}
console.log('Kullanıcı oluşturuldu:', result.value.id);
}
Zod ile Validasyon ve Hata Yönetimi
import { z } from 'zod';
import { Result, ok, err } from './lib/result';
const UserSchema = z.object({
email: z.string().email('Geçerli email giriniz'),
name: z.string().min(2, 'İsim en az 2 karakter olmalı'),
age: z.number().min(18, '18 yaşından büyük olmalısınız'),
});
type UserInput = z.infer<typeof UserSchema>;
interface ValidationFailure {
type: 'VALIDATION';
errors: Record<string, string[]>;
}
function validateUser(
input: unknown
): Result<UserInput, ValidationFailure> {
const parsed = UserSchema.safeParse(input);
if (!parsed.success) {
const errors: Record<string, string[]> = {};
parsed.error.issues.forEach((issue) => {
const path = issue.path.join('.');
if (!errors[path]) errors[path] = [];
errors[path].push(issue.message);
});
return err({ type: 'VALIDATION', errors });
}
return ok(parsed.data);
}
Tip Güvenli Error Boundary (React)
// components/ErrorBoundary.tsx
import { Component, ErrorInfo, ReactNode } from 'react';
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
interface ErrorBoundaryProps {
children: ReactNode;
fallback: (error: Error, reset: () => void) => ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
}
export class ErrorBoundary extends Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
state: ErrorBoundaryState = { hasError: false, error: null };
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
this.props.onError?.(error, errorInfo);
}
reset = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError && this.state.error) {
return this.props.fallback(this.state.error, this.reset);
}
return this.props.children;
}
}
Exhaustive Error Handling
TypeScript'in never tipi ile tüm hata durumlarının ele alındığından emin olabilirsiniz:
type ApiError =
| { code: 'NOT_FOUND'; resource: string }
| { code: 'UNAUTHORIZED' }
| { code: 'RATE_LIMITED'; retryAfter: number }
| { code: 'SERVER_ERROR'; message: string };
function assertNever(value: never): never {
throw new Error(`Beklenmeyen değer: ${JSON.stringify(value)}`);
}
function handleApiError(error: ApiError): string {
switch (error.code) {
case 'NOT_FOUND':
return `${error.resource} bulunamadı`;
case 'UNAUTHORIZED':
return 'Lütfen giriş yapın';
case 'RATE_LIMITED':
return `Çok fazla istek. ${error.retryAfter}s sonra tekrar deneyin`;
case 'SERVER_ERROR':
return `Sunucu hatası: ${error.message}`;
default:
return assertNever(error);
// Yeni bir hata türü eklendiğinde TypeScript burada hata verir
}
}
assertNever kontrolü, discriminated union'a yeni bir varyant eklendiğinde derleme hatası vererek tüm switch-case'lerin güncellenmesini zorunlu kılar. Bu pattern'ı kullanmak, hataların gözden kaçmasını önler.
Async Hata Yönetimi Wrapper
// lib/tryCatch.ts
async function tryCatch<T>(
promise: Promise<T>
): Promise<Result<T, Error>> {
try {
const value = await promise;
return ok(value);
} catch (error) {
return err(error instanceof Error ? error : new Error(String(error)));
}
}
// Kullanım
async function fetchUserData(userId: string) {
const result = await tryCatch(
fetch(`/api/users/${userId}`).then((r) => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json() as Promise<User>;
})
);
if (!result.ok) {
console.error('Veri çekme hatası:', result.error.message);
return null;
}
return result.value;
}
Sonuç
TypeScript'te tip güvenli hata yönetimi, uygulamanızın güvenilirliğini büyük ölçüde artırır. Custom error sınıfları ile hata türlerini kategorize edin, Result pattern ile hataları fonksiyon imzalarının parçası yapın, exhaustive checking ile tüm hata durumlarının ele alındığından emin olun. Exception'lar beklenmeyen durumlar için, Result pattern ise beklenen hata senaryoları için kullanılmalıdır.