Sparround

Exception vs Result: hansını, niyə

Exception-larla işləmək tək qatlı kodda problem deyil. Problem qatlar arasında başlayır və rəsmi Flutter sənədi onu üç bəndlə sayır:

  • Exception-lar sənədləşdirilmir. Fərqli qatlar və komponentlər sənədləşdirilməmiş exception ata bilir; çağıran tərəf nəyi tutmalı olduğunu bilmir.
  • Developer tutmağı unudur. Kompilyator xəbərdarlıq etmir; nəticədə tutulmamış exception istifadəçiyə crash kimi çatır.
  • `try/catch` bloklarının içi-içinə düşür. Bir neçə ardıcıl əməliyyat olduqda nəzarət axını oxunmaz olur.

Result sinfi eyni sənəddə bu üç problemin cavabı kimi təqdim olunur: çağıran tərəfi xətanı yoxlamağa məcbur edir, ona görə tutulmamış exception-lardan doğan baqların sayı azalır.

Məntiq sadədir: exception gizli çıxış yoludur (imzada görünmür), Result isə açıq — o, funksiyanın qaytarma tipindədir və kompilyator onu nəzərə almağa məcbur edir.

MeyarException`Result<T>`
İmzada görünür?Xeyr — `Future<Order>` xəta barədə heç nə demirBəli — `Future<Result<Order>>`
Kompilyator məcbur edir?XeyrBəli — `sealed` tipdə `switch` tam olmalıdır
Ardıcıl 3 əməliyyatİçi-içinə `try/catch`Ardıcıl yoxlama, düz axın
Kod həcmiAz — "uğurlu yol" təmiz qalırÇox — hər çağırışda yoxlama
Proqramçı səhvləri (`Error`)Uyğundur — proqram dayanmalıdırUyğun deyil — belə səhvi bükmək baqı gizlədir
Qat sərhədləriSızmağa meyllidirSərhəddə dayanır

Vacib bir ayırma var: Dart-da `Error` və `Exception` fərqli məqsədlər üçündür.

Error sinfinin rəsmi sənədi birbaşadır: Error obyektləri proqramçının qaçınmalı olduğu proqram nasazlığını təmsil edir; bunlar çağıranın gözləməli ya da tutmalı olduğu xətalar deyil, və baş verirsə proqramın dayandırılması ən təhlükəsiz cavab ola bilər. Nümunələr: RangeError, StateError, TypeError, AssertionError.

Bundan praktik qayda çıxır:

  • `Result` yalnız gözlənilən uğursuzluqlar üçündür: şəbəkə yoxdur, 404, token bitib, validasiya alınmadı, ödəniş rədd edildi.
  • `Error` bükülmür. RangeError-u Result.error() içinə salmaq baqı gizlədir: siyahının indeksi səhvdirsə, düzəliş kodda olmalıdır, ekranda "xəta baş verdi" mesajında deyil.
  • Rəsmi Result implementasiyasının Error variantı da məhz Exception tipini daşıyır — Object deyil. Bu, təsadüfi deyil.

Bir nüans: catch (e) hər şeyi tutur, o cümlədən Error-ları. Ona görə repository-də on SocketException, on UnauthorizedException kimi tipli tutma yazmaq daha düzgündür; catch (e) yalnız ən yuxarı səviyyədə (loqlama üçün) qalır.

dart
// ══════ EXCEPTION ILƏ ══════
abstract interface class OrderRepository {
  // İmza: xəta barədə heç nə demir.
  // Hansı exception-lar atılır? Sənəddə yazılmasa — bilinmir.
  Future<Order> place(Cart cart);
}

// Notifier-də: nə tutmalı olduğunu təxmin edirsən.
Future<void> submit(Cart cart) async {
  state = const AsyncValue.loading();
  try {
    final order = await _repository.place(cart);
    state = AsyncValue.data(order);
  } on OutOfStockException catch (e) {      // bunu bilirdin
    state = AsyncValue.error(e, StackTrace.current);
  } on PaymentDeclinedException catch (e) { // bunu da
    state = AsyncValue.error(e, StackTrace.current);
  }
  // Bəs `SocketException`? Bəs `UnauthorizedException`?
  // Tutulmadı → tətbiq crash olur, ya da state loading-də qalır.
}


// ══════ RESULT ILƏ ══════
abstract interface class OrderRepository {
  // İmza: uğursuzluğun mümkün olduğunu AÇIQ deyir.
  Future<Result<Order>> place(Cart cart);
}

// Notifier-də: kompilyator hər iki halı nəzərə almağa məcbur edir.
Future<void> submit(Cart cart) async {
  state = const AsyncValue.loading();
  final result = await _repository.place(cart);

  // `sealed` tip: `switch` tam olmalıdır, yoxsa kompilyasiya xətası.
  state = switch (result) {
    Ok(:final value) => AsyncValue.data(value),
    Error(:final error) => AsyncValue.error(error, StackTrace.current),
  };
}


// ══════ ARDICIL ƏMƏLİYYATLAR: ən böyük fərq ══════
// Exception ilə: içi-içinə try/catch
Future<void> checkoutWithExceptions(Cart cart) async {
  try {
    final reserved = await _inventory.reserve(cart);
    try {
      final payment = await _payments.charge(reserved.total);
      try {
        await _orders.confirm(reserved, payment);
      } catch (e) {
        await _payments.refund(payment);   // kompensasiya
        rethrow;
      }
    } catch (e) {
      await _inventory.release(reserved);  // kompensasiya
      rethrow;
    }
  } catch (e) {
    // Burada hansı addım sındı? Bilinmir.
  }
}

// Result ilə: düz axın, hər addım aydın
Future<Result<Order>> checkoutWithResult(Cart cart) async {
  // Tam `switch` ifadəsi dəyəri çıxarır və uğursuzluqda dərhal qaytarır.
  // (`reserved` yalnız bir budaqda təyin olunur — Dart bunu qəbul edir,
  //  çünki digər budaq `return` edir.)
  final Reservation reserved;
  switch (await _inventory.reserve(cart)) {
    case Ok(:final value):
      reserved = value;
    case Error(:final error):
      return Result.error(error);
  }

  final Payment payment;
  switch (await _payments.charge(reserved.total)) {
    case Ok(:final value):
      payment = value;
    case Error(:final error):
      await _inventory.release(reserved);          // kompensasiya
      return Result.error(error);
  }

  switch (await _orders.confirm(reserved, payment)) {
    case Ok(:final value):
      return Result.ok(value);
    case Error(:final error):
      await _payments.refund(payment);             // kompensasiya
      await _inventory.release(reserved);
      return Result.error(error);
  }
}

Eyni ssenari iki cür: exception ilə (nə tutulacağı görünmür) və `Result` ilə (imza hər şeyi deyir).

Result hər yerdə lazım deyil. Praktik bölgü belədir:

  • Service qatı — exception atır (UnauthorizedException, ServerException). Burada Result əlavə etmək faydasızdır: service nazikdir və onun çağıranı yalnız repository-dir.
  • Repository qatı — exception-ları tutur və Result (ya da Failure) qaytarır. Bu, sərhəddir.
  • Domain (use-case)Result ötürür ya da birləşdirir.
  • PresentationResult-u AsyncValue-a ya da state-ə çevirir.

Praktika. Layihəndə bir repository metodunu Result qaytarmağa keçir: exception-ları repository-nin içində tut, Result.ok/Result.error qaytar, notifier-də switch yaz. Sonra bir test əlavə et: şəbəkə xətası halında Result.error gəlir və exception atılmır.

Hazır sayılır: notifier-də try/catch qalmayıb, test yaşıldır.

📚 Mənbələr və sənədlər

  • Result obyektləri ilə xəta idarəsirəsmidocs.flutter.dev

    Bu mövzunun əsas mənbəyi: exception-ların üç problemi və `Result`-un onları necə həll etməsi.

  • Dart: `Error` sinfirəsmiapi.dart.dev

    "Proqramçının qaçınmalı olduğu nasazlıq" — `Error`-un tutulmamalı olduğunun rəsmi izahı.

  • Dart: xəta idarəsirəsmidart.dev

    `on` və `catch` fərqi, `rethrow` — tipli tutmanın sintaksisi.