Takip et

RxJS’te Basit Hata Yönetimi: Akışlarınızı Güvende Tutmak

RxJS’te Basit Hata Yönetimi: Akışlarınızı Güvende Tutmak RxJS, reaktif programlamanın gücünü JavaScript dünyasına taşıyan, asenkron ve

RxJS’te Basit Hata Yönetimi: Akışlarınızı Güvende Tutmak

RxJS, reaktif programlamanın gücünü JavaScript dünyasına taşıyan, asenkron veri akışlarını yönetmek için kapsamlı bir kütüphanedir. Ancak herhangi bir asenkron sistemde olduğu gibi, RxJS akışlarında da hatalar kaçınılmazdır. Ağ isteklerinin başarısız olması, veri işleme sırasında beklenmeyen durumlar veya harici servislerin yanıt vermemesi gibi birçok senaryoda hatalarla karşılaşabiliriz. Bu tür durumları etkili bir şekilde yönetmek, uygulamalarımızın kararlılığını, kullanıcı deneyimini ve genel güvenilirliğini sağlamak için hayati öneme sahiptir.

Bu makalede, RxJS’teki basit ama güçlü hata yönetimi mekanizmalarını derinlemesine inceleyeceğiz. Akışlarda hataların nasıl yayıldığını, bu hataları yakalamak, kurtarmak ve hatta yeniden denemek için kullanabileceğimiz temel operatörleri ve en iyi uygulamaları ele alacağız. Amacımız, RxJS ile geliştirme yaparken hata yönetimini bir kabus olmaktan çıkarıp, akışlarınızı sağlam ve öngörülebilir hale getiren bir araç setine dönüştürmektir.

RxJS Akışlarında Hataları Anlamak

RxJS’te bir Observable (gözlemlenebilir) üç tür bildirim yayabilir: next (yeni bir değer), complete (akışın başarıyla tamamlanması) ve error (akışta bir hata oluşması). Bir Observable bir error bildirimi yaydığında, bu durum akışın sona erdiği anlamına gelir. Yani, hata sonrası Observable artık next veya complete bildirimi yaymaz. Bu “akış sonlandırma” davranışı, hata yönetimini özel kılar; çünkü bir hata meydana geldiğinde, akış varsayılan olarak durur ve subscribe metodunun error geri çağrısı tetiklenir. Eğer bu hata yakalanmazsa, genellikle uygulamanın genel hata işleyicisi (örneğin, tarayıcıda window.onerror veya Node.js’te process.on('uncaughtException')) tarafından ele alınır ve bu durum uygulamanın beklenmedik bir şekilde çökmesine neden olabilir.

RxJS’teki hata yönetimi operatörleri, bu varsayılan akış sonlandırma davranışını değiştirmemizi veya bu duruma tepki vermemizi sağlar. Temel amaç, bir hata meydana geldiğinde akışı tamamen durdurmak yerine, akışı kurtarmak, alternatif bir yol sunmak veya işlemi yeniden denemek için bir fırsat yaratmaktır.

Temel Hata Yönetimi Operatörleri

RxJS, hata yönetimi için bir dizi güçlü operatör sunar. Bu operatörler, farklı hata senaryolarına uygun çözümler sunar.

catchError

catchError, RxJS’teki en temel ve en sık kullanılan hata yönetimi operatörüdür. Bir kaynak Observable‘dan gelen herhangi bir hatayı yakalar ve bu hatayı işleyerek yerine yeni bir Observable döndürmenize olanak tanır. Bu sayede, hata durumunda akışın tamamen sonlanmasını engeller ve alternatif bir veri akışı sunar.

Nasıl Çalışır?
catchError operatörü, bir hata meydana geldiğinde çağrılan bir fonksiyon alır. Bu fonksiyon iki argüman alır:
1. error: Yakalanan hata nesnesi.
2. caught: Hatayı yakalayan Observable‘ın kendisi. Bu, bazen hatayı yeniden fırlatmak veya başka bir işlem yapmak için kullanılabilir.

Fonksiyonun, bir Observable döndürmesi beklenir. Bu döndürülen Observable, orijinal akışın yerine geçer ve aboneliği devam ettirir.

İmza:

catchError((error: any, caught: Observable) => ObservableInput)

Kullanım Durumları:

1. Varsayılan Değer veya Yedek Akış Döndürme: Bir ağ isteği başarısız olduğunda, kullanıcıya boş bir dizi veya önbellekten alınmış veriler gibi bir varsayılan değer sunmak isteyebilirsiniz.

import { of, throwError } from 'rxjs';
    import { catchError } from 'rxjs/operators';

    function fetchData(shouldFail: boolean) {
      return shouldFail ? throwError(() => new Error('Veri yüklenemedi!')) : of('Gerçek Veri');
    }

    fetchData(true).pipe(
      catchError(error => {
        console.error('Hata yakalandı:', error.message);
        return of('Yedek Veri'); // Hata durumunda alternatif bir Observable döndür
      })
    ).subscribe({
      next: data => console.log('Alınan Veri:', data),
      error: err => console.error('Abone hatası:', err.message), // Bu kısım çalışmaz çünkü hata yakalandı
      complete: () => console.log('Akış tamamlandı.')
    });

    // Çıktı:
    // Hata yakalandı: Veri yüklenemedi!
    // Alınan Veri: Yedek Veri
    // Akış tamamlandı.

2. Hatayı Kaydetme ve Yeniden Fırlatma: Bazen hatayı kaydetmek (loglamak) ancak yine de akışın sonlanmasına izin vermek isteyebilirsiniz. Bu durumda, throwError operatörünü kullanarak hatayı yeniden fırlatabilirsiniz.

import { throwError } from 'rxjs';
    import { catchError, tap } from 'rxjs/operators';

    function riskyOperation(shouldFail: boolean) {
      return shouldFail ? throwError(() => new Error('Kritik Hata!')) : of('Başarılı İşlem');
    }

    riskyOperation(true).pipe(
      catchError(error => {
        console.error('Kritik hata loglandı:', error.message);
        return throwError(() => new Error('İşlem başarısız oldu: ' + error.message)); // Hatayı yeniden fırlat
      })
    ).subscribe({
      next: data => console.log('Veri:', data),
      error: err => console.error('Abone hatası yakalandı:', err.message),
      complete: () => console.log('Akış tamamlandı.')
    });

    // Çıktı:
    // Kritik hata loglandı: Kritik Hata!
    // Abone hatası yakalandı: İşlem başarısız oldu: Kritik Hata!

Bu senaryoda, catchError sadece bir yan etki (logging) için kullanıldı ve akış yine de hata ile sonlandı.

3. Hata Durumunda Hiçbir Şey Yapmama (Akışı Tamamlama): Eğer bir hata durumunda akışın sessizce tamamlanmasını istiyorsanız, EMPTY Observable’ını döndürebilirsiniz. Ancak bu, hatanın göz ardı edildiği anlamına geldiği için dikkatli kullanılmalıdır.

import { EMPTY, throwError } from 'rxjs';
    import { catchError } from 'rxjs/operators';

    throwError(() => new Error('Önemli olmayan hata')).pipe(
      catchError(error => {
        console.warn('Önemli olmayan hata yoksayıldı:', error.message);
        return EMPTY; // Akışı tamamla, hiçbir değer yayma
      })
    ).subscribe({
      next: data => console.log('Veri:', data),
      error: err => console.error('Abone hatası:', err.message),
      complete: () => console.log('Akış tamamlandı (EMPTY).')
    });

    // Çıktı:
    // Önemli olmayan hata yoksayıldı: Önemli olmayan hata
    // Akış tamamlandı (EMPTY).

retry

retry operatörü, bir hata meydana geldiğinde kaynak Observable‘a belirli sayıda yeniden abone olmaya çalışır. Bu, özellikle geçici ağ hataları veya kısa süreli servis kesintileri gibi durumlarda oldukça faydalıdır.

Nasıl Çalışır?
retry operatörü, bir sayı veya yapılandırma nesnesi alabilir. Sayı belirtilirse, o kadar kez yeniden deneme yapar. Yapılandırma nesnesi ile gecikme, karar verme fonksiyonu gibi daha gelişmiş seçenekler sunar.

İmza:

retry(count?: number | RetryConfig)

Kullanım Durumları:

1. Belirli Sayıda Yeniden Deneme: Bir operasyonun 3 kez yeniden denenmesini sağlamak.

import { timer, throwError } from 'rxjs';
    import { mergeMap, retry, tap } from 'rxjs/operators';

    let attemptCount = 0;

    function unstableService() {
      return timer(1000).pipe( // 1 saniye sonra değer yay
        mergeMap(() => {
          attemptCount++;
          if (attemptCount < 3) {
            console.log(Deneme ${attemptCount}: Hata oluştu, yeniden deniyor...);
            return throwError(() => new Error('Geçici servis hatası'));
          } else {
            console.log(Deneme ${attemptCount}: Başarılı!);
            return of('Servis Verisi');
          }
        })
      );
    }

    unstableService().pipe(
      retry(2) // Toplamda 1 orijinal deneme + 2 yeniden deneme = 3 deneme
    ).subscribe({
      next: data => console.log('Alınan Veri:', data),
      error: err => console.error('Abone hatası:', err.message),
      complete: () => console.log('Akış tamamlandı.')
    });

    // Çıktı:
    // Deneme 1: Hata oluştu, yeniden deniyor...
    // Deneme 2: Hata oluştu, yeniden deniyor...
    // Deneme 3: Başarılı!
    // Alınan Veri: Servis Verisi
    // Akış tamamlandı.

2. Gecikmeli Yeniden Deneme: Hata durumunda hemen yeniden denemek yerine, belirli bir süre bekledikten sonra yeniden denemek isteyebilirsiniz. Bu, sunucunun kendini toparlaması için zaman tanır.

import { timer, throwError, of } from 'rxjs';
    import { mergeMap, retry, tap } from 'rxjs/operators';

    let attemptCountWithDelay = 0;

    function unstableServiceWithDelay() {
      return timer(500).pipe( // 0.5 saniye sonra değer yay
        mergeMap(() => {
          attemptCountWithDelay++;
          if (attemptCountWithDelay < 3) {
            console.log(Gecikmeli Deneme ${attemptCountWithDelay}: Hata oluştu.);
            return throwError(() => new Error('Geçici servis hatası (gecikmeli)'));
          } else {
            console.log(Gecikmeli Deneme ${attemptCountWithDelay}: Başarılı!);
            return of('Gecikmeli Servis Verisi');
          }
        })
      );
    }

    unstableServiceWithDelay().pipe(
      retry({
        count: 2, // Toplamda 3 deneme
        delay: 1000 // Her yeniden deneme öncesi 1 saniye bekle
      })
    ).subscribe({
      next: data => console.log('Alınan Veri (gecikmeli):', data),
      error: err => console.error('Abone hatası (gecikmeli):', err.message),
      complete: () => console.log('Akış tamamlandı (gecikmeli).')
    });

    // Çıktı:
    // Gecikmeli Deneme 1: Hata oluştu.
    // (1 saniye bekleme)
    // Gecikmeli Deneme 2: Hata oluştu.
    // (1 saniye bekleme)
    // Gecikmeli Deneme 3: Başarılı!
    // Alınan Veri (gecikmeli): Gecikmeli Servis Verisi
    // Akış tamamlandı (gecikmeli).

retryWhen

retryWhen operatörü, retry‘dan daha gelişmiş yeniden deneme stratejileri oluşturmak için kullanılır. Bir hata meydana geldiğinde ne zaman ve nasıl yeniden deneneceğine dair özel bir mantık uygulamanıza olanak tanır. Genellikle üstel geri çekilme (exponential backoff) gibi karmaşık senaryolar için kullanılır.

Nasıl Çalışır?
retryWhen, bir fonksiyon alır. Bu fonksiyon, bir hata akışı (Observable) alır ve yeniden deneme sinyali veren bir Observable döndürür. Bu döndürülen Observable bir değer yaydığında yeniden deneme tetiklenir. Eğer bu Observable bir hata yayarsa, orijinal akış da hata ile sonlanır. Eğer complete olursa, orijinal akış da tamamlanır.

İmza:

retryWhen(notifier: (errors: Observable) => Observable)

Kullanım Durumları:

1. Üstel Geri Çekilme (Exponential Backoff): Hata durumunda yeniden deneme aralığını her seferinde artırmak.

import { timer, throwError, of } from 'rxjs';
    import { mergeMap, retryWhen, delay, take, tap } from 'rxjs/operators';

    let retryAttempt = 0;

    function flakyService() {
      return timer(500).pipe(
        mergeMap(() => {
          retryAttempt++;
          if (retryAttempt < 4) {
            console.log(Flaky servis denemesi ${retryAttempt}: Hata oluştu.);
            return throwError(() => new Error('Servis geçici olarak kullanılamıyor.'));
          } else {
            console.log(Flaky servis denemesi ${retryAttempt}: Başarılı!);
            return of('Flaky Servis Verisi');
          }
        })
      );
    }

    flakyService().pipe(
      retryWhen(errors =>
        errors.pipe(
          tap(err => console.error('Hata akışı:', err.message)),
          delay(1000), // İlk gecikme
          take(3) // Maksimum 3 yeniden deneme (toplam 4 deneme)
        )
      )
    ).subscribe({
      next: data => console.log('Alınan Veri (flaky):', data),
      error: err => console.error('Abone hatası (flaky):', err.message),
      complete: () => console.log('Akış tamamlandı (flaky).')
    });

    // Çıktı:
    // Flaky servis denemesi 1: Hata oluştu.
    // Hata akışı: Servis geçici olarak kullanılamıyor.
    // (1 saniye bekleme)
    // Flaky servis denemesi 2: Hata oluştu.
    // Hata akışı: Servis geçici olarak kullanılamıyor.
    // (1 saniye bekleme)
    // Flaky servis denemesi 3: Hata oluştu.
    // Hata akışı: Servis geçici olarak kullanılamıyor.
    // (1 saniye bekleme)
    // Flaky servis denemesi 4: Başarılı!
    // Alınan Veri (flaky): Flaky Servis Verisi
    // Akış tamamlandı (flaky).

Bu örnekte, her hata sonrası 1 saniye gecikmeyle yeniden deneme yapılıyor. take(3) ile 3 kez yeniden deneme sınırı konuluyor. Daha karmaşık senaryolarda, delay operatörüne bir fonksiyon vererek gecikme süresini dinamik olarak artırabilirsiniz (üstel geri çekilme).

finalize

finalize operatörü, bir Observable akışı tamamlandığında (başarıyla veya hata ile) veya abonelik iptal edildiğinde çalışan bir geri çağırma (callback) fonksiyonu tanımlamanızı sağlar. Hata yönetimi için doğrudan bir mekanizma olmasa da, hata durumunda kaynakları serbest bırakmak veya UI durumunu güncellemek gibi temizleme işlemleri için kritik öneme sahiptir.

Nasıl Çalışır?
finalize operatörü, akışın herhangi bir şekilde sona ermesi durumunda (next, error veya complete ile) veya aboneliğin iptal edilmesi durumunda çağrılan tek bir fonksiyon alır.

İmza:

finalize(callback: () => void)

Kullanım Durumları:

1. Yükleme Göstergelerini Gizleme: Bir ağ isteği başladığında bir yükleme göstergesi gösterip, istek tamamlandığında veya hata verdiğinde gizlemek.

import { of, throwError } from 'rxjs';
    import { finalize, tap } from 'rxjs/operators';

    function simulateDataLoad(shouldFail: boolean) {
      console.log('Yükleme başladı...');
      return shouldFail ? throwError(() => new Error('Yükleme hatası')) : of('Yüklenen Veri').pipe(delay(1000));
    }

    simulateDataLoad(true).pipe(
      finalize(() => console.log('Yükleme tamamlandı (hata veya başarı).')),
      catchError(error => {
        console.error('Hata yakalandı:', error.message);
        return of('Varsayılan Veri');
      })
    ).subscribe({
      next: data => console.log('Veri:', data),
      error: err => console.error('Abone hatası:', err.message),
      complete: () => console.log('Akış tamamlandı.')
    });

    // Çıktı (hata durumunda):
    // Yükleme başladı...
    // Yükleme tamamlandı (hata veya başarı).
    // Hata yakalandı: Yükleme hatası
    // Veri: Varsayılan Veri
    // Akış tamamlandı.

    // Çıktı (başarılı durumda):
    // Yükleme başladı...
    // (1 saniye bekleme)
    // Yükleme tamamlandı (hata veya başarı).
    // Veri: Yüklenen Veri
    // Akış tamamlandı.

onErrorResumeNext (Kısa Bahis)

onErrorResumeNext operatörü, bir Observable hata verdiğinde, bir sonraki Observable‘a abone olarak akışı devam ettirmenizi sağlar. Bu, birden fazla yedek veri kaynağı denemek gibi durumlarda kullanılabilir. Ancak, genellikle catchError ile daha esnek çözümler üretilebilir ve onErrorResumeNext daha az yaygın kullanılır.

import { of, throwError, onErrorResumeNext } from 'rxjs';
import { delay } from 'rxjs/operators';

const source1 = throwError(() => new Error('Kaynak 1 Hatası'));
const source2 = of('Kaynak 2 Verisi').pipe(delay(500));
const source3 = of('Kaynak 3 Verisi');

onErrorResumeNext(source1, source2, source3).subscribe({
  next: val => console.log('Alınan:', val),
  error: err => console.error('Hata:', err),
  complete: () => console.log('Tamamlandı.')
});

// Çıktı:
// Alınan: Kaynak 2 Verisi
// Alınan: Kaynak 3 Verisi
// Tamamlandı.

Bu örnekte, source1 hata verdiğinde, onErrorResumeNext otomatik olarak source2‘ye geçer. source2 başarıyla tamamlandığında ise source3‘e geçer.

Global Hata Yönetimi

Bazı durumlarda, bir Observable‘daki hata catchError gibi operatörler tarafından yakalanmaz ve akışın en üst seviyesine kadar yükselir. Bu tür yakalanmayan hataları izlemek veya özel olarak ele almak için RxJS’in global hata işleyicisini kullanabiliriz.

RxJS, config.onUnhandledError özelliğini sunar (RxJS 6’dan itibaren rxjs/internal/config veya doğrudan rxjs‘den erişilebilir). Bu, yakalanmayan hatalar için genel bir geri çağırma fonksiyonu tanımlamanıza olanak tanır.

import { throwError, of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { config } from 'rxjs'; // RxJS 6+ için

// Global hata işleyiciyi tanımlama
config.onUnhandledError = (err) => {
  console.error('Global yakalanmayan hata:', err);
  // Burada hata raporlama servisine gönderme, UI'da bildirim gösterme vb. yapılabilir.
};

// Hata yakalanmayan bir Observable
throwError(() => new Error('Yakalanmayan bir hata!')).pipe(
  delay(100) // Asenkron hale getir
).subscribe({
  next: val => console.log('Veri:', val),
  complete: () => console.log('Tamamlandı.')
});

// Çıktı (yaklaşık 100ms sonra):
// Global yakalanmayan hata: Error: Yakalanmayan bir hata!

Bu mekanizma, uygulamanızdaki herhangi bir Observable akışında gözden kaçan hataları tespit etmek ve merkezi bir yerde ele almak için çok değerlidir. Ancak, bu işleyici hatayı yakalar ama akışın durmasını engellemez; sadece hatanın bildirimini sağlar.

Hata Yönetimi İçin En İyi Uygulamalar

RxJS’te etkili hata yönetimi, sadece operatörleri bilmekle kalmaz, aynı zamanda belirli prensiplere uymayı da gerektirir:

1. Hataları Erken Yakalayın: Mümkünse, hatanın kaynağına en yakın yerde catchError kullanın. Bu, hatanın daha geniş akışları etkilemesini engeller ve hatanın bağlamını korur. Örneğin, bir API çağrısının hemen sonrasında.

2. Spesifik Olun: Yalnızca beklediğiniz ve yönetebileceğiniz hataları yakalayın. Genel bir catchError kullanmak yerine, belirli hata türleri için özel işleyiciler yazmayı düşünün.

3. Hataları Kaydedin (Loglama): Her zaman yakalanan veya yakalanmayan hataları kaydettiğinizden emin olun. Bu, sorun giderme ve uygulamanızın sağlığını izlemek için kritik öneme sahiptir. tap operatörü, yan etkiler (logging gibi) için kullanılabilir.

4. Kullanıcıya Geri Bildirim Sağlayın: Kullanıcı arayüzünde hatalar hakkında anlamlı geri bildirimler sağlayın. “Bir hata oluştu” yerine “Veriler yüklenemedi, lütfen daha sonra tekrar deneyin” gibi mesajlar daha faydalıdır.

5. Hataları “Yutmaktan” Kaçının: Sadece catchError(() => EMPTY) veya catchError(() => of(null)) kullanarak hatayı sessizce yok saymayın. Eğer hatayı işlemeyecekseniz bile, en azından loglayın veya kullanıcıya bildirin. Aksi takdirde, uygulamanızda tespit edilmesi zor hatalar gizlenebilir.

6. Yeniden Denemelerde İdempotensi Dikkate Alın: retry veya retryWhen kullanırken, yeniden denediğiniz işlemin birden fazla kez güvenli bir şekilde çalıştırılabileceğinden emin olun (idempotent). Örneğin, bir ödeme işlemini yeniden denemek, çifte ödemeye yol açabilir.

7. Akış Sonlandırmayı Anlayın: catchError kullanmadığınız sürece, bir hata akışı sonlandırır. Bu, aboneliğin sona erdiği ve daha fazla değerin yayılmayacağı anlamına gelir. Bu davranışı göz önünde bulundurarak akışlarınızı tasarlayın.

8. İç İçe Observables’daki Hatalar: switchMap, mergeMap, concatMap gibi operatörler içinde dönen iç Observable‘ların hatalarını da yakalamayı unutmayın. Aksi takdirde, iç Observable‘daki bir hata, dış Observable‘ı da sonlandırabilir.

import { of, throwError } from 'rxjs';
    import { switchMap, catchError } from 'rxjs/operators';

    of(1).pipe(
      switchMap(() =>
        throwError(() => new Error('İç Observable hatası!')) // İç Observable'da hata
      ),
      catchError(error => {
        console.error('Dış Observable tarafından yakalanan hata:', error.message);
        return of('Yedek değer');
      })
    ).subscribe({
      next: val => console.log('Değer:', val),
      error: err => console.error('Abone hatası:', err.message)
    });

    // Çıktı:
    // Dış Observable tarafından yakalanan hata: İç Observable hatası!
    // Değer: Yedek değer

Bu örnekte, switchMap içindeki hata, dış akışı da sonlandıracağı için catchError dış Observable‘a uygulanarak hata yakalanmıştır.

Sonuç

RxJS, asenkron programlamayı basitleştiren güçlü bir araçtır, ancak reaktif akışlarda hata yönetimi, uygulamanızın kararlılığı ve güvenilirliği için kritik bir bileşendir. catchError, retry, retryWhen ve finalize gibi operatörler, hataları yakalama, kurtarma, yeniden deneme ve temizleme işlemleri için size esnek ve güçlü mekanizmalar sunar.

Bu operatörleri doğru bir şekilde kullanarak ve yukarıda belirtilen en iyi uygulamaları takip ederek, RxJS akışlarınızı hatalara karşı daha dirençli hale getirebilirsiniz. Unutmayın ki sağlam bir hata yönetimi stratejisi, sadece kodunuzu daha güvenilir kılmakla kalmaz, aynı zamanda geliştirme sürecinde hataları daha kolay tespit etmenize ve gidermenize de yardımcı olur. RxJS’in sunduğu bu araçları ustaca kullanarak, daha robust ve kullanıcı dostu reaktif uygulamalar inşa edebilirsiniz.

Yorumlar
İçeriği beğendiniz mi? Bir tartışma başlatın veya görüşlerinizi paylaşın.
Yorum Yaz

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

E-posta Bülteni
Yazılım Topluluğuna Katılın
En son güncellemeleri, yaratıcı ipuçlarını ve özel kaynakları doğrudan e-posta kutunuza alın. Tasarım ve inovasyonun geleceğini birlikte keşfedelim.