İçeriğe geç / Skip to content / Zum Inhalt

Dart'ta Hata Yönetimi: Try-Catch ve Asenkron İşlemler

Ahmet Balaman

Son güncelleme:

6 dk okuma

FlutterDartTry CatchAsyncAwaitFutureHata Yönetimi
Dart'ta Hata Yönetimi: Try-Catch ve Asenkron İşlemler

Hata yönetimi ve asenkron kod, öğrencilerin "çalışıyor" ile "sağlam çalışıyor" arasındaki farkı ilk kez hissettiği konulardır. Derslerde en sık gördüğüm iki alışkanlık şunlar: her şeyi catch (e) ile yakalayıp hatayı sessizce yutmak ve bir async fonksiyonu await etmeyi unutup hatanın neden yakalanmadığını anlamamak. Bu yazıda ikisini de temelden ele alıyoruz.

Collections yazısından sonra Dart temellerinin son büyük parçası bu. Örnekler Dart 3 ile yazıldı.

Canlı Demo: Try-Catch ve Async-Await

Hata yönetimi ve asenkron programlamayı interaktif olarak öğrenin:

Try-Catch: Hataları Yakalamak

Programlar her zaman beklendiği gibi çalışmaz: kullanıcı yanlış bir şey girer, dosya bulunamaz, bağlantı kopar. Konsoldan sayı okuyan şu programa bakalım:

import 'dart:io';

void main() {
  print('Bir sayı girin:');
  final girdi = stdin.readLineSync() ?? '';
  final sayi = int.parse(girdi); // "abc" girilirse FormatException
  print('Girdiğiniz sayı: $sayi');
}

Kullanıcı "abc" yazarsa int.parse bir FormatException fırlatır ve program durur. Bu davranışı tip dönüşümleri yazısında görmüştük. (stdin yalnızca konsol uygulamalarında çalışır; DartPad'de denemek için girdiyi bir değişkene yazın.)

on ile Hedefli Yakalama

void main() {
  const girdi = 'abc';

  try {
    final sayi = int.parse(girdi);
    print('Girdiğiniz sayı: $sayi');
  } on FormatException catch (e) {
    print('Lütfen geçerli bir sayı girin. (${e.message})');
  }
}

on FormatException yalnızca o tipteki hatayı yakalar. Eski örneklerde sıkça görülen çıplak catch (e) ise fırlatılan her şeyi yakalar; programlama hatalarınızı da. Effective Dart rehberi de on içermeyen catch bloklarından kaçınmayı önerir. Beklediğiniz hatayı adıyla yakalayın; beklemediğiniz hata görünür kalsın ki düzeltebilesiniz.

Birden fazla hata tipini ayrı ayrı ele alabilirsiniz. Sıra önemlidir: özel olan önce, genel olan sonra gelir:

void isle(String girdi) {
  try {
    final sayi = int.parse(girdi);
    if (sayi < 0) throw ArgumentError.value(sayi, 'girdi', 'Negatif olamaz');
    print('Sonuç: ${100 ~/ sayi}');
  } on FormatException {
    print('Sayı değil.');
  } on ArgumentError catch (e) {
    print('Geçersiz değer: ${e.message}');
  } catch (e, yigin) {
    print('Beklenmeyen hata: $e');
    print(yigin); // Stack trace: hatanın nerede oluştuğunu gösterir
    rethrow;      // Hatayı yutmadan yukarı iletir
  }
}

Buradaki üç ayrıntıya dikkat edin:

  • catch (e, yigin) ikinci parametre olarak stack trace'i verir. Hata ayıklarken hatanın kendisi kadar değerlidir.
  • rethrow, yakaladığınız hatayı orijinal stack trace'iyle birlikte yeniden fırlatır. Kayıt tutup hatayı üst katmana bırakmak istediğinizde kullanılır. throw e yazmak ise stack trace'i kaybettirir.
  • ArgumentError bir Error'dır. Dart'ta Exception beklenen, çalışma zamanında olabilecek durumları (geçersiz girdi, ağ hatası), Error ise programcı hatalarını (yanlış argüman, null'a erişim) temsil eder. Genel kural olarak Error'ları yakalayıp devam etmek yerine kodu düzeltirsiniz; buradaki on ArgumentError yalnızca iki türü yan yana göstermek için var.

Girdi Geçerli Olana Kadar Tekrar Sormak

import 'dart:io';

void main() {
  int? sayi;

  while (sayi == null) {
    print('Bir sayı girin:');
    sayi = int.tryParse(stdin.readLineSync() ?? '');
    if (sayi == null) print('Lütfen geçerli bir sayı girin!');
  }

  print('Tebrikler! Girdiğiniz sayı: $sayi');
}

Bu örneğin eski sürümü döngü içinde try-catch kullanıyordu. Geçersiz girdi burada istisnai değil, beklenen bir durum olduğu için tryParse daha uygun: hata fırlatıp yakalamak yerine null kontrolü yeterli.

finally Bloğu

finally bloğu, hata olsa da olmasa da çalışır:

void main() {
  try {
    print('Bağlantı açıldı');
    int.parse('abc'); // FormatException
  } on FormatException catch (e) {
    print('Hata: ${e.message}');
  } finally {
    print('Bağlantı kapatıldı'); // Her durumda çalışır
  }
}

Dosya, veritabanı bağlantısı ya da bir yükleniyor göstergesi gibi "her durumda kapatılması gereken" şeyler için kullanılır.

Kendi Exception Sınıfınız

Uygulamanıza özgü hatalar için Exception'ı uygulayan bir sınıf yazabilirsiniz. Böylece çağıran kod onu on ile adıyla yakalayabilir:

class YetersizBakiye implements Exception {
  final double eksik;
  const YetersizBakiye(this.eksik);

  @override
  String toString() => 'YetersizBakiye: $eksik TL eksik';
}

double odemeYap(double bakiye, double tutar) {
  if (tutar > bakiye) throw YetersizBakiye(tutar - bakiye);
  return bakiye - tutar;
}

void main() {
  try {
    odemeYap(100, 150);
  } on YetersizBakiye catch (e) {
    print('Ödeme yapılamadı, ${e.eksik} TL eksik.'); // Ödeme yapılamadı, 50.0 TL eksik.
  }
}

throw 'Bir hata oluştu' gibi String fırlatmak da derlenir, ama yakalayan tarafın elinde tip bilgisi kalmaz. Anlamlı bir sınıf fırlatın.

Asenkron İşlemler: async ve await

Bazı işlemler zaman alır: ağdan veri çekmek, dosya okumak, veritabanına yazmak. Bu işlemleri beklerken uygulamanın donmasını istemeyiz.

Zaman çizgisinde senkron akış iki işlemi arka arkaya çalıştırır, asenkron akış ikisini üst üste bindirir ve daha erken biter

Burada sık yapılan bir kavram hatasını baştan düzeltelim: Dart'ta asenkron kod, ayrı bir iş parçacığında (thread) çalışmaz. Dart tek bir olay döngüsü (event loop) üzerinde çalışır. await yalnızca o fonksiyonu duraklatır ve beklenen iş (örneğin ağ cevabı) gelene kadar olay döngüsünün başka işleri yapmasına izin verir. Beklenen iş ağ ya da dosya gibi dışarıda gerçekleşen bir şeyse, arayüz bu sırada akıcı kalır. Ama ağır bir hesaplamayı async fonksiyona koymak onu hızlandırmaz ve arayüzü yine dondurur; öyle işler için Isolate.run ile ayrı bir isolate kullanılır.

Future, async, await

  • Future: Değeri ileride hazır olacak bir işlemin temsili. İşlem ya bir değerle ya da bir hatayla tamamlanır.
  • async: Fonksiyonun içinde await kullanılabileceğini belirtir; fonksiyon otomatik olarak bir Future döndürür.
  • await: Future tamamlanana kadar o fonksiyonu bekletir ve sonucu verir.
Future<void> main() async {
  print('Veri bekleniyor...');

  final veri = await veritabanindanVeriAl();

  print('Veri alındı: $veri');
}

Future<String> veritabanindanVeriAl() async {
  // Veritabanı gecikmesini simüle ediyoruz
  await Future.delayed(const Duration(seconds: 3));
  return 'Kullanıcı verileri';
}

await Unutulursa Ne Olur?

Future<void> main() async {
  // String veri = veritabanindanVeriAl(); // Derleme hatası: Future<String>, String değildir
  final veri = veritabanindanVeriAl();     // Derlenir ama veri bir Future
  print(veri); // Instance of 'Future<String>'
}

İlk satır derlenmez, çünkü fonksiyon değeri değil, değerin ileride geleceği sözünü (Future) döndürür. İkinci satır daha sinsidir: final ile tip çıkarımı Future<String> olur, kod derlenir ve ekrana veri yerine Instance of 'Future<String>' yazılır. Derslerde bu çıktıyı gördüğünüzde ilk bakacağınız yer eksik bir await'tir.

İlerleme Gösteren Bir Örnek

Future<void> main() async {
  print('1. Veri bekleniyor...');
  final veri = await veritabanindanVeriAl();
  print('2. Veri alındı: $veri');
}

Future<String> veritabanindanVeriAl() async {
  for (var i = 1; i <= 5; i++) {
    await Future.delayed(const Duration(seconds: 1));
    print('Yükleniyor... %${i * 20}');
  }
  return 'Veritabanı kümesi';
}

Çıktı:

1. Veri bekleniyor...
Yükleniyor... %20
Yükleniyor... %40
Yükleniyor... %60
Yükleniyor... %80
Yükleniyor... %100
2. Veri alındı: Veritabanı kümesi

Asenkron Hataları Yakalamak

await edilen bir Future hatayla tamamlanırsa, hata await satırında fırlatılır ve normal bir try-catch ile yakalanır:

import 'dart:async';

class SunucuHatasi implements Exception {
  final String mesaj;
  const SunucuHatasi(this.mesaj);
}

Future<String> apidenVeriCek() async {
  await Future.delayed(const Duration(seconds: 2));
  throw const SunucuHatasi('Sunucuya bağlanılamadı');
}

Future<void> main() async {
  try {
    print("API'den veri çekiliyor...");
    final veri = await apidenVeriCek().timeout(const Duration(seconds: 5));
    print('Veri: $veri');
  } on TimeoutException {
    print('Sunucu zamanında cevap vermedi.');
  } on SunucuHatasi catch (e) {
    print('Hata: ${e.mesaj}');
  }
}

timeout, belirtilen sürede tamamlanmayan Future'ı TimeoutException ile sonlandırır. Ağ isteklerinde bir zaman aşımı tanımlamak, kullanıcının sonsuza kadar yükleniyor göstergesine bakmasını engeller.

await Edilmeyen Future ve unawaited

await edilmeyen bir Future'ın hatası, çevresindeki try-catch tarafından yakalanmaz. Try bloğu Future'ı başlatır ve hemen biter; hata ise daha sonra oluşur:

Future<void> main() async {
  try {
    apidenVeriCek(); // await yok: hata burada YAKALANMAZ
  } on SunucuHatasi {
    print('Buraya hiç gelinmez');
  }
}

Böyle bir hata, uygulamanın yakalanmamış hata işleyicisine düşer. Çözüm çoğu zaman basittir: await ekleyin. Bir işlemi bilerek beklemek istemiyorsanız (örneğin arka planda bir kayıt gönderimi), bunu dart:async içindeki unawaited ile açıkça belirtin ve hatayı Future'ın kendisinde ele alın:

import 'dart:async';

Future<void> kayitGonder(String olay) async {
  // Arka planda bir analiz kaydı gönderildiğini düşünelim
}

void butonaBasildi() {
  unawaited(
    kayitGonder('buton').catchError((Object e) => print('Kayıt gönderilemedi: $e')),
  );
}

unawaited, okuyana ve analyzer'a "bunu beklememem bilinçli" mesajını verir. catchError'ın geri çağrısı, Future'ın tipine uygun bir değer döndürmelidir; burada tip void olduğu için print yeterli.

Paralel Bekleme

Birbirine bağlı olmayan işlemleri sırayla await etmek, sürelerini toplar:

Future<String> kullaniciGetir() async {
  await Future.delayed(const Duration(seconds: 1));
  return 'Ayşe';
}

Future<int> bildirimSayisiGetir() async {
  await Future.delayed(const Duration(seconds: 1));
  return 3;
}

Future<void> main() async {
  // Sırayla: yaklaşık 2 saniye
  final ad = await kullaniciGetir();
  final sayi = await bildirimSayisiGetir();

  // Paralel: yaklaşık 1 saniye
  final (ad2, sayi2) = await (kullaniciGetir(), bildirimSayisiGetir()).wait;

  print('$ad $sayi / $ad2 $sayi2'); // Ayşe 3 / Ayşe 3
}

Dart 3'teki record'lar sayesinde farklı tipte Future'ları (a, b).wait ile birlikte bekleyip sonucu tek satırda ayrıştırabilirsiniz. Future'lardan biri hata verirse .wait bir ParallelWaitError fırlatır. Aynı tipte çok sayıda Future için klasik Future.wait([...]) da kullanılır ve sonuçları bir List olarak verir.

Flutter'da Asenkron Kod

Flutter'da asenkron kod her yerdedir: API çağrıları, yerel veritabanı, dosya okuma, izin istekleri. En sık kalıp, initState içinde veri yükleyip sonucu setState ile göstermektir:

import 'package:flutter/material.dart';

class VeriSayfasi extends StatefulWidget {
  const VeriSayfasi({super.key});

  @override
  State<VeriSayfasi> createState() => _VeriSayfasiState();
}

class _VeriSayfasiState extends State<VeriSayfasi> {
  String _veri = 'Yükleniyor...';

  @override
  void initState() {
    super.initState();
    _verileriYukle();
  }

  Future<void> _verileriYukle() async {
    try {
      // Gerçek uygulamada burada bir API çağrısı olur
      await Future.delayed(const Duration(seconds: 2));
      if (!mounted) return; // Sayfa bu sırada kapatıldıysa dur
      setState(() => _veri = 'Veriler başarıyla yüklendi!');
    } on Exception catch (e) {
      if (!mounted) return;
      setState(() => _veri = 'Hata: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(child: Text(_veri)),
    );
  }
}

if (!mounted) return; satırı, eski örneklerde eksik olan önemli bir güvenliktir. Kullanıcı veri gelmeden sayfadan çıkarsa widget ağaçtan kaldırılır; await'ten sonra kontrol etmeden setState çağırmak hataya yol açar. Her await'ten sonra widget'ın hâlâ ekranda olup olmadığını sorun.

Buradaki Future.delayed gerçek bir ağ isteğinin yerini tutuyor. http paketiyle gerçek bir API'ye istek atmayı, durum kodlarını kontrol etmeyi ve zaman aşımı eklemeyi HTTP istekleri ve REST API yazısında adım adım anlattım.

FutureBuilder

Flutter, bir Future'ın durumuna göre arayüz çizen hazır bir widget sunar:

class ProfilSayfasi extends StatefulWidget {
  const ProfilSayfasi({super.key});

  @override
  State<ProfilSayfasi> createState() => _ProfilSayfasiState();
}

class _ProfilSayfasiState extends State<ProfilSayfasi> {
  late final Future<String> _veri = veritabanindanVeriAl(); // Bir kez oluşturulur

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<String>(
      future: _veri,
      builder: (context, snapshot) {
        if (snapshot.connectionState != ConnectionState.done) {
          return const CircularProgressIndicator();
        }
        if (snapshot.hasError) {
          return Text('Hata: ${snapshot.error}');
        }
        return Text('Veri: ${snapshot.data}');
      },
    );
  }
}

Future'ın build içinde değil, State'in bir alanında oluşturulduğuna dikkat edin. future: veritabanindanVeriAl() yazılırsa her yeniden çizimde istek baştan atılır. Ayrıntılı kullanımını FutureBuilder yazısında anlattım; tek seferlik değil sürekli akan veri için de kardeşi StreamBuilder var.

Kurallar ve Sık Yapılan Hatalar

  1. await yalnızca async fonksiyonlarda kullanılır.
// YANLIŞ
void fonksiyon() {
  await birSey(); // Derleme hatası
}

// DOĞRU
Future<void> fonksiyon() async {
  await birSey();
}
  1. async fonksiyonların dönüş tipi Future olmalı. void f() async yazımı derlenir, ama çağıran taraf fonksiyonu bekleyemez ve hatasını yakalayamaz. Değer döndürmeyen asenkron fonksiyonlar için Future<void> yazın.

  2. Bağımsız işlemleri sırayla beklemeyin. Yukarıdaki record .wait ya da Future.wait ile paralel başlatın.

  3. Hataları adıyla yakalayın. on FormatException, on TimeoutException, on SunucuHatasi; çıplak catch (e) yalnızca en sonda, kayıt tutup rethrow etmek için.

  4. Hatayı sessizce yutmayın. Boş bir catch bloğu, sorunu çözmez, yalnızca gizler.

  5. await'ten sonra mounted kontrol edin. Flutter'da widget bu arada ağaçtan kalkmış olabilir.

Sonraki Adım

Dart'ın kod tarafı tamam; sıradaki durak arayüz: Flutter'ın widget ağacı, Row, Column ve Stack yapıları.

Kaynaklar


Sorularınız mı var?

Sık Sorulan Sorular

catch (e) ile on FormatException catch (e) arasındaki fark nedir?

catch (e) fırlatılan her şeyi yakalar; on FormatException catch (e) ise yalnızca o tipteki hatayı. Belirli hataları önce on ile ayrı ayrı ele alıp genel catch'i en sona koymak, beklenmedik hataları yanlışlıkla yutmanızı engeller.

await yazmadan çağırdığım async fonksiyonun hatası neden catch'e düşmüyor?

await olmadan fonksiyon hemen bir Future döndürür ve hata o Future'ın içinde oluşur; try bloğu o sırada çoktan bitmiştir. Çağrının önüne await ekleyin ya da sonucu bilerek beklemeyecekseniz unawaited ile işaretleyip hatayı .catchError ile Future üzerinde yakalayın.

throw e ile rethrow arasındaki fark nedir?

rethrow yakalanan hatayı orijinal stack trace'iyle birlikte yeniden fırlatır. throw e ise hatayı yeni bir noktadan fırlatmış gibi davranır ve hatanın ilk oluştuğu yerin bilgisi kaybolur. Catch bloğunda hatayı yukarı iletmek istiyorsanız rethrow kullanın.

FutureBuilder'ın future parametresine neden doğrudan fonksiyon çağrısı vermemeliyim?

build her yeniden çizimde çalıştığı için future: veriAl() yazarsanız istek her seferinde baştan atılır. Future'ı State'in bir alanında (örneğin initState içinde ya da late final ile) bir kez oluşturup FutureBuilder'a o alanı verin.

Yorumlar