Dart'ta Hata Yönetimi: Try-Catch ve Asenkron İşlemler
Son güncelleme:
6 dk okuma

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 eyazmak ise stack trace'i kaybettirir.ArgumentErrorbirError'dır. Dart'taExceptionbeklenen, çalışma zamanında olabilecek durumları (geçersiz girdi, ağ hatası),Errorise programcı hatalarını (yanlış argüman, null'a erişim) temsil eder. Genel kural olarakError'ları yakalayıp devam etmek yerine kodu düzeltirsiniz; buradakion ArgumentErroryalnı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.
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
awaitkullanılabileceğini belirtir; fonksiyon otomatik olarak birFuturedö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ümesiAsenkron 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
awaityalnızcaasyncfonksiyonlarda kullanılır.
// YANLIŞ
void fonksiyon() {
await birSey(); // Derleme hatası
}
// DOĞRU
Future<void> fonksiyon() async {
await birSey();
}async fonksiyonların dönüş tipi
Futureolmalı.void f() asyncyazımı derlenir, ama çağıran taraf fonksiyonu bekleyemez ve hatasını yakalayamaz. Değer döndürmeyen asenkron fonksiyonlar içinFuture<void>yazın.Bağımsız işlemleri sırayla beklemeyin. Yukarıdaki record
.waitya daFuture.waitile paralel başlatın.Hataları adıyla yakalayın.
on FormatException,on TimeoutException,on SunucuHatasi; çıplakcatch (e)yalnızca en sonda, kayıt tutuprethrowetmek için.Hatayı sessizce yutmayın. Boş bir
catchbloğu, sorunu çözmez, yalnızca gizler.await'ten sonramountedkontrol 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
- Dart: Error handling
- Dart: Asynchronous programming (futures, async, await)
- Effective Dart: Usage (Error handling)
- Flutter: Handling errors
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.
İlgili Yazılar
Flutter: FutureBuilder ile Asenkron Listeleme
Flutter FutureBuilder rehberi: snapshot durumlarını doğru okuma, Future'ı build içinde oluşturma hatası, initState çözümü ve setState ile yenileme.
Flutter: SharedPreferences ile Yerel Veri Saklama
Flutter'da shared_preferences ile tema ve ayar saklama: SharedPreferencesAsync, WithCache ve eski API farkı, geçiş aracı ve orada saklanmaması gerekenler.
Flutter: HTTP İstekleri ve REST API Kullanımı
Flutter'da http paketiyle GET, POST, PUT ve DELETE, JSON'dan model sınıfı, durum kodları, zaman aşımı ve FutureBuilder ile yükleniyor ve hata ekranı.