Takip et

JavaScript’te Promise’lar: ES6/ES2015 ile Asenkron Programlamanın Gücü

JavaScript’te Promise’lar: ES6/ES2015 ile Asenkron Programlamanın Gücü JavaScript, doğası gereği tek iş parçacıklı (single-threaded) bir di

JavaScript’te Promise’lar: ES6/ES2015 ile Asenkron Programlamanın Gücü

JavaScript, doğası gereği tek iş parçacıklı (single-threaded) bir dildir. Bu, bir anda yalnızca bir görevi yerine getirebileceği anlamına gelir. Ancak web uygulamaları genellikle ağ istekleri yapma, dosya okuma/yazma, zamanlayıcılar kullanma gibi uzun süreli ve zaman alıcı işlemleri içerir. Bu tür işlemler senkronize bir şekilde yürütüldüğünde, ana iş parçacığını bloke eder ve kullanıcı arayüzünün donmasına, uygulamanın yanıt veremez hale gelmesine neden olur. İşte bu noktada asenkron programlama devreye girer.

ES6 (ECMAScript 2015) ile birlikte JavaScript’e eklenen Promise’lar, asenkron işlemleri yönetmek için modern, güçlü ve daha okunabilir bir yöntem sunar. Promise’lar, gelecekte tamamlanacak veya başarısız olacak bir işlemin nihai sonucunu temsil eden bir nesnedir. Geleneksel callback tabanlı yaklaşımların (callback hell olarak bilinen iç içe callback’ler) neden olduğu karmaşıklığı gidererek, daha düzenli ve yönetilebilir bir kod yapısı sağlar.

Asenkron Programlamanın Temelleri ve İhtiyaç Duyulan Çözümler

JavaScript’in tek iş parçacıklı yapısı göz önüne alındığında, uzun süren işlemlerin ana iş parçacığını bloke etmeden çalışabilmesi kritik öneme sahiptir. Geleneksel olarak bu, callback fonksiyonları aracılığıyla yapılıyordu. Bir işlem tamamlandığında veya bir hata oluştuğunda çalıştırılacak bir fonksiyonu başka bir fonksiyona parametre olarak geçmek, asenkronluğun temelini oluşturuyordu.

// Callback Hell Örneği (Geleneksel Yaklaşım)
veriAl(function(veri) {
  veriyiIsle(veri, function(islenmisVeri) {
    veriyiKaydet(islenmisVeri, function(sonuc) {
      console.log('İşlem tamamlandı:', sonuc);
    }, function(hata) {
      console.error('Kaydetme hatası:', hata);
    });
  }, function(hata) {
    console.error('İşleme hatası:', hata);
  });
}, function(hata) {
  console.error('Veri alma hatası:', hata);
});

Yukarıdaki örnekte görüldüğü gibi, iç içe geçmiş callback’ler kodu okumayı, hata ayıklamayı ve sürdürmeyi zorlaştırır. Bu duruma “callback hell” veya “pyramid of doom” denir. Promise’lar, bu sorunu çözmek için tasarlanmıştır.

Promise Nedir?

Promise, henüz bilinmeyen bir değer için bir proxy’dir. Asenkron bir işlemin başarılı sonucunu veya başarısızlık nedenini temsil eder. Bir Promise, özetle, gelecekte bir değerle yerine getirilebilecek veya bir nedenle reddedilebilecek bir taahhüttür.

Bir Promise’ın üç ana durumu (state) vardır:

1. Pending (Beklemede): Başlangıç durumu. Ne yerine getirilmiş ne de reddedilmiştir. Asenkron işlem hala devam etmektedir.
2. Fulfilled (Yerine Getirildi/Resolved): İşlem başarıyla tamamlandı ve Promise bir değer döndürdü.
3. Rejected (Reddedildi): İşlem başarısız oldu ve Promise bir hata nedeni (reason) döndürdü.

Bir Promise, “pending” durumundan “fulfilled” veya “rejected” durumuna geçebilir. Bu geçişe “settled” (sonuçlandı) denir. Bir Promise bir kez sonuçlandığında, durumu ve değeri/nedeni bir daha asla değişmez. Bu, Promise’ların önemli bir özelliğidir: durumları değişmezdir (immutable).

Promise Oluşturma

Yeni bir Promise oluşturmak için new Promise() yapıcısını kullanırız. Bu yapıcı, iki argüman alan bir fonksiyon alır: resolve ve reject.

const myPromise = new Promise((resolve, reject) => {
  // Asenkron bir işlem simüle edelim
  const success = Math.random() > 0.5; // Rastgele başarı/başarısızlık

  setTimeout(() => {
    if (success) {
      resolve('İşlem başarıyla tamamlandı!'); // Başarılı olursa resolve çağrılır
    } else {
      reject(new Error('İşlem başarısız oldu!')); // Başarısız olursa reject çağrılır
    }
  }, 2000); // 2 saniye sonra sonuçlanacak
});

console.log('Promise oluşturuldu, bekleniyor...');

* resolve(value): İşlemin başarılı olduğunu ve Promise’ın value ile yerine getirildiğini belirtir.
* reject(reason): İşlemin başarısız olduğunu ve Promise’ın reason (genellikle bir Error nesnesi) ile reddedildiğini belirtir.

Promise Tüketme: .then(), .catch() ve .finally()

Bir Promise oluşturulduktan sonra, onun sonucunu (başarı veya hata) dinlememiz gerekir. Bu, .then(), .catch() ve .finally() metotları aracılığıyla yapılır.

.then() Metodu

.then() metodu, bir Promise başarıyla yerine getirildiğinde veya reddedildiğinde çalıştırılacak callback fonksiyonlarını kaydetmek için kullanılır. İki opsiyonel argüman alır:

1. onFulfilled (veya onResolved): Promise yerine getirildiğinde çağrılan fonksiyon. Promise’ın başarı değerini argüman olarak alır.
2. onRejected: Promise reddedildiğinde çağrılan fonksiyon. Promise’ın hata nedenini argüman olarak alır.

myPromise.then(
  (message) => {
    console.log('Başarılı:', message); // onFulfilled callback'i
  },
  (error) => {
    console.error('Hata:', error.message); // onRejected callback'i
  }
);

Genellikle onRejected callback’ini .then() içinde vermek yerine, hataları ayrı bir .catch() bloğunda ele almak daha iyi bir uygulamadır. Bu, kodun daha okunabilir olmasını sağlar ve Promise zincirindeki herhangi bir yerdeki hatayı yakalayabilir.

.catch() Metodu

.catch() metodu, aslında .then(null, onRejected) ‘in bir kısaltmasıdır ve sadece Promise reddedildiğinde çalıştırılacak bir callback fonksiyonu kaydetmek için kullanılır. Promise zincirindeki herhangi bir önceki Promise’dan gelen hataları yakalamak için idealdir.

myPromise
  .then((message) => {
    console.log('Başarılı:', message);
  })
  .catch((error) => {
    console.error('Hata yakalandı:', error.message); // Promise zincirindeki herhangi bir hatayı yakalar
  });

.finally() Metodu (ES2018)

.finally() metodu, Promise’ın yerine getirilmiş veya reddedilmiş olmasına bakılmaksızın, Promise sonuçlandığında çalıştırılacak bir callback fonksiyonu kaydeder. Bu, temizlik işlemleri (örneğin, bir yükleme göstergesini gizleme) için çok kullanışlıdır. .finally() callback’i herhangi bir argüman almaz ve kendisi bir değer döndürmez.

myPromise
  .then((message) => {
    console.log('Başarılı:', message);
  })
  .catch((error) => {
    console.error('Hata yakalandı:', error.message);
  })
  .finally(() => {
    console.log('Promise sonuçlandı, temizlik yapılıyor...'); // Her durumda çalışır
  });

Promise Zincirleme (Chaining)

Promise’ların en güçlü özelliklerinden biri, zincirlenebilme yetenekleridir. .then() metodu her zaman yeni bir Promise döndürür. Bu, birden fazla asenkron işlemi sırayla yürütmek için Promise’ları art arda çağırabileceğimiz anlamına gelir.

Bir .then() callback’inden bir değer döndürdüğünüzde, bu değer bir sonraki .then() callback’ine argüman olarak iletilir.

function adim1() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('Adım 1 tamamlandı');
      resolve(10); // 10 değerini döndür
    }, 1000);
  });
}

function adim2(oncekiDeger) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('Adım 2 tamamlandı, önceki değer:', oncekiDeger);
      resolve(oncekiDeger * 2); // Önceki değeri ikiyle çarp
    }, 1000);
  });
}

function adim3(oncekiDeger) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('Adım 3 tamamlandı, önceki değer:', oncekiDeger);
      if (oncekiDeger > 15) {
        resolve('Tüm adımlar başarıyla tamamlandı, sonuç: ' + oncekiDeger);
      } else {
        reject(new Error('Adım 3 başarısız: Değer çok küçük!'));
      }
    }, 1000);
  });
}

adim1()
  .then(adim2) // adim1'in sonucunu adim2'ye ilet
  .then(adim3) // adim2'nin sonucunu adim3'e ilet
  .then((finalSonuc) => {
    console.log('Nihai Sonuç:', finalSonuc);
  })
  .catch((hata) => {
    console.error('Zincirleme sırasında bir hata oluştu:', hata.message);
  });

Eğer bir .then() callback’inden bir Promise döndürürseniz, zincir o Promise’ın sonuçlanmasını bekler ve ardından devam eder. Bu, iç içe Promise’ları düzleştirmek (flattening) için çok önemlidir ve “callback hell” sorununu ortadan kaldırır.

fetch('/api/users/1') // Birinci Promise
  .then((response) => response.json()) // İkinci Promise (response.json() da bir Promise döndürür)
  .then((user) => fetch(/api/posts?userId=${user.id})) // Üçüncü Promise
  .then((response) => response.json())
  .then((posts) => {
    console.log('Kullanıcının gönderileri:', posts);
  })
  .catch((error) => {
    console.error('Veri çekme hatası:', error);
  });

Bu örnekte, fetch API’si (ki kendisi bir Promise döndürür) ile kullanıcı verilerini çekiyor, ardından bu kullanıcının ID’sini kullanarak gönderilerini çekiyoruz. Her .then() bloğu bir önceki Promise’ın sonucunu işler ve bir sonraki asenkron işlemi başlatır.

Promise’larda Hata Yönetimi

Promise zincirlemesinde hata yönetimi oldukça etkilidir. Bir Promise zincirinde herhangi bir noktada bir Promise reddedilirse (veya bir .then() callback’i içinde bir hata fırlatılırsa), zincirdeki sonraki başarılı .then() callback’leri atlanır ve kontrol, zincirdeki ilk .catch() callback’ine geçer.

new Promise((resolve, reject) => {
  console.log('Başlangıç');
  // throw new Error('Beklenmedik bir hata!'); // Bu da yakalanır
  reject(new Error('İlk Promise reddedildi!'));
})
  .then(() => {
    console.log('Bu blok çalışmayacak.');
    return 'Başarılı adım';
  })
  .then((data) => {
    console.log('Bu da çalışmayacak:', data);
  })
  .catch((error) => {
    console.error('Hata yakalandı:', error.message); // Burası çalışacak
    // Hatanın ardından bir değer döndürerek zinciri devam ettirebiliriz
    return 'Hata sonrası devam eden değer';
  })
  .then((data) => {
    console.log('Hata sonrası devam eden blok:', data); // Burası çalışacak
  })
  .finally(() => {
    console.log('Her zaman çalışır');
  });

Bir .catch() bloğu, yakaladığı hatayı ele aldıktan sonra bir değer döndürürse, zincir başarıyla devam eder. Eğer .catch() bloğu bir hata fırlatırsa veya bir Promise’ı reddederse, hata zincirde aşağı doğru yayılmaya devam eder.

Önemli: Promise’lar içinde oluşan yakalanmayan hatalar (unhandled rejections), tarayıcı konsolunda uyarı olarak görünür ve Node.js ortamında uygulamanın çökmesine neden olabilir. Bu nedenle her Promise zincirinin sonunda bir .catch() bloğu bulundurmak iyi bir uygulamadır.

Statik Promise Metotları

Promise API’si, birden fazla Promise ile çalışmak için çeşitli statik metotlar sunar:

Promise.all(iterable)

Promise.all(), bir Promise dizisi (veya herhangi bir iterable) alır ve tüm Promise’lar başarıyla yerine getirildiğinde kendisi de yerine getirilen yeni bir Promise döndürür. Eğer dizideki Promise’lardan herhangi biri reddedilirse, Promise.all() hemen reddedilir ve reddedilen ilk Promise’ın hata nedenini döndürür.

Başarılı durumda, döndürülen Promise, girdi dizisindeki Promise’ların yerine getirilen değerlerini içeren bir dizi ile çözümlenir (sıralama orijinal diziyle aynıdır).

const promise1 = Promise.resolve(3);
const promise2 = 42; // Promise olmayan değerler de kabul edilir, hemen çözümlenir
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3])
  .then((values) => {
    console.log(values); // [3, 42, "foo"]
  })
  .catch((error) => {
    console.error('Promise.all hatası:', error);
  });

// Hata örneği
const p1 = new Promise((resolve) => setTimeout(() => resolve('P1'), 100));
const p2 = new Promise((_, reject) => setTimeout(() => reject(new Error('P2 hatası')), 50)); // Daha hızlı reddedilir
const p3 = new Promise((resolve) => setTimeout(() => resolve('P3'), 200));

Promise.all([p1, p2, p3])
  .then((values) => console.log(values))
  .catch((error) => console.error(error.message)); // P2 hatası

Promise.all(), birden fazla bağımsız asenkron işlemi paralel olarak başlatmak ve hepsinin tamamlanmasını beklemek istediğiniz durumlarda kullanışlıdır.

Promise.race(iterable)

Promise.race(), bir Promise dizisi alır ve dizideki Promise’lardan herhangi biri yerine getirilir veya reddedilirse, kendisi de aynı şekilde sonuçlanan bir Promise döndürür. Yani, “yarışı” ilk bitiren Promise’ın sonucunu (değer veya hata) döndürür.

const pRace1 = new Promise((resolve) => setTimeout(() => resolve('Birinci'), 500));
const pRace2 = new Promise((resolve) => setTimeout(() => resolve('İkinci'), 100)); // Daha hızlı

Promise.race([pRace1, pRace2])
  .then((value) => {
    console.log(value); // "İkinci"
  })
  .catch((error) => {
    console.error('Promise.race hatası:', error);
  });

// Hata örneği
const pRace3 = new Promise((resolve) => setTimeout(() => resolve('Başarılı'), 500));
const pRace4 = new Promise((_, reject) => setTimeout(() => reject(new Error('Hata oluştu')), 100)); // Daha hızlı reddedilir

Promise.race([pRace3, pRace4])
  .then((value) => console.log(value))
  .catch((error) => console.error(error.message)); // Hata oluştu

Promise.race(), bir zaman aşımı (timeout) uygulamak veya birden fazla kaynaktan veri çekmeye çalışırken en hızlı yanıtı almak gibi durumlarda kullanılabilir.

Promise.resolve(value)

Promise.resolve(value), verilen value ile hemen yerine getirilen bir Promise döndürür. Eğer value zaten bir Promise ise, o Promise’ın kendisini döndürür. Bu, bir değeri Promise’a dönüştürmek veya zaten sahip olduğunuz bir değeri bir Promise zincirine dahil etmek için kullanışlıdır.

Promise.resolve('Merhaba dünya!')
  .then((value) => console.log(value)); // Merhaba dünya!

const p = Promise.resolve(new Promise((resolve) => setTimeout(() => resolve('Zaten Promise'), 500)));
p.then((value) => console.log(value)); // Zaten Promise

Promise.reject(reason)

Promise.reject(reason), verilen reason ile hemen reddedilen bir Promise döndürür.

Promise.reject(new Error('Hata mesajı'))
  .catch((error) => console.error(error.message)); // Hata mesajı

Promise.allSettled(iterable) (ES2020)

Promise.allSettled(), Promise.all()‘a benzer şekilde bir Promise dizisi alır, ancak tüm Promise’ların yerine getirilmesini veya reddedilmesini bekler. Yani, tüm Promise’lar “settled” (sonuçlanmış) olduğunda kendisi de yerine getirilen bir Promise döndürür.

Döndürülen Promise, her bir orijinal Promise’ın sonucunu açıklayan bir nesneler dizisi ile çözümlenir. Her nesne, Promise’ın durumunu (status: ‘fulfilled’ veya ‘rejected’) ve ilgili değerini (value) veya nedenini (reason) içerir.

const p1Settled = new Promise((resolve) => setTimeout(() => resolve('P1 başarılı'), 100));
const p2Settled = new Promise((_, reject) => setTimeout(() => reject(new Error('P2 başarısız')), 200));
const p3Settled = Promise.resolve('P3 tamam');

Promise.allSettled([p1Settled, p2Settled, p3Settled])
  .then((results) => {
    console.log(results);
    /*
    [
      { status: 'fulfilled', value: 'P1 başarılı' },
      { status: 'rejected', reason: Error: P2 başarısız },
      { status: 'fulfilled', value: 'P3 tamam' }
    ]
    */
  });

Promise.allSettled(), tüm paralel görevlerin sonuçlarını bilmeniz gerektiğinde (bazıları başarısız olsa bile) çok kullanışlıdır.

Promise.any(iterable) (ES2021)

Promise.any(), bir Promise dizisi alır ve dizideki Promise’lardan herhangi biri yerine getirildiğinde kendisi de yerine getirilen bir Promise döndürür. Eğer dizideki tüm Promise’lar reddedilirse, Promise.any() bir AggregateError ile reddedilir ve bu hata, tüm reddedilme nedenlerini içerir.

const pAny1 = new Promise((_, reject) => setTimeout(() => reject(new Error('Hata 1')), 500));
const pAny2 = new Promise((resolve) => setTimeout(() => resolve('Başarılı 2'), 100)); // İlk başarılı olan
const pAny3 = new Promise((_, reject) => setTimeout(() => reject(new Error('Hata 3')), 200));

Promise.any([pAny1, pAny2, pAny3])
  .then((value) => {
    console.log(value); // "Başarılı 2"
  })
  .catch((error) => {
    console.error('Promise.any hatası:', error.errors); // Tüm hataları AggregateError içinde gösterir
  });

Promise.any(), birden fazla kaynaktan veri çekmeye çalışırken ve sadece ilk başarılı sonucu elde etmek istediğinizde kullanışlıdır.

Promises ve Async/Await (ES2017)

ES2017 ile tanıtılan async/await sentaktik şekeri, Promise’lar üzerine inşa edilmiştir ve asenkron kodu senkron kod gibi yazmayı mümkün kılar. async anahtar kelimesi, bir fonksiyonun her zaman bir Promise döndüreceğini belirtir. await anahtar kelimesi ise sadece async fonksiyonlar içinde kullanılabilir ve bir Promise’ın çözümlenmesini bekler, ardından Promise’ın değerini döndürür veya reddedilirse bir hata fırlatır.

async/await, Promise’ları doğrudan kullanmaktan daha okunabilir ve bakımı kolay kod yazmayı sağlar, özellikle karmaşık asenkron akışlarda. Ancak, async/await‘in temelinde hala Promise’lar yatmaktadır, bu yüzden Promise’ların nasıl çalıştığını anlamak async/await‘i doğru bir şekilde kullanmak için hayati öneme sahiptir.

async function fetchUserData() {
  try {
    const userResponse = await fetch('/api/users/1');
    const user = await userResponse.json();

    const postsResponse = await fetch(/api/posts?userId=${user.id});
    const posts = await postsResponse.json();

    console.log('Kullanıcı:', user);
    console.log('Gönderileri:', posts);
  } catch (error) {
    console.error('Veri çekme hatası:', error);
  } finally {
    console.log('Veri çekme işlemi tamamlandı.');
  }
}

fetchUserData();

Bu async/await örneği, önceki Promise zincirleme örneğiyle aynı işi yapar, ancak çok daha düz ve okunabilir bir şekilde. Hata yönetimi için try...catch blokları kullanılır.

Promise Kullanımında En İyi Uygulamalar ve Yaygın Tuzaklar

* Her Zaman return kullanın: Bir .then() callback’inden bir Promise veya bir değer döndürmeyi unutmayın. Aksi takdirde zincir kopar ve bir sonraki .then() callback’i undefined bir değerle çalışır.
* Hata Yönetimini İhmal Etmeyin: Her Promise zincirinin sonunda bir .catch() bloğu bulundurmak, uygulamanızın beklenmedik hatalar karşısında daha sağlam olmasını sağlar.
* Gereksiz İç İçe Kullanımdan Kaçının: Promise’ların amacı callback hell’den kaçınmaktır. Promise’ları iç içe kullanmak yerine zincirleme yapın.
* Paralel Görevler için Promise.all() Kullanın: Birbirinden bağımsız birden fazla asenkron görevi paralel olarak çalıştırmak ve tümünün bitmesini beklemek için Promise.all() idealdir.
* Promise Yapıcısı İçinde Senkron Hataları Yönetin: new Promise() yapıcısı içindeki senkron kodda oluşan hatalar otomatik olarak reject çağrısı gibi davranır. Ancak yine de try...catch kullanmak, hata ayıklama sırasında daha net bir niyet belirtir.
* Promise’ları İptal Etme: Promise’ların yerleşik bir iptal mekanizması yoktur. Eğer bir Promise’ı iptal etmeniz gerekiyorsa, bu durumu kendi mantığınızla yönetmeniz (örneğin, bir bayrak kullanarak veya AbortController gibi daha yeni API’leri kullanarak) gerekir.

Sonuç

JavaScript Promise’ları, ES6/ES2015 ile birlikte asenkron programlamayı kökten değiştirerek daha temiz, daha yönetilebilir ve daha güçlü bir yapı sunmuştur. Callback hell’in karmaşıklığından kurtulmamızı sağlayarak, asenkron işlemleri mantıksal bir sıra içinde organize etmemize olanak tanır. .then(), .catch() ve .finally() metotları ile Promise zincirleme yeteneği, karmaşık iş akışlarını basitleştirir. Promise.all(), Promise.race() gibi statik metotlar, birden fazla Promise ile çalışmayı kolaylaştırır.

async/await gibi daha yeni özellikler Promise’ların üzerine inşa edilmiş olsa da, Promise’ların temel prensiplerini ve davranışlarını anlamak modern JavaScript geliştirme için vazgeçilmezdir. Promise’lar, web uygulamalarının performansını ve kullanıcı deneyimini artıran, güvenilir ve okunabilir asenkron kod yazmanın temel taşıdır. Her JavaScript geliştiricisinin Promise’ları derinlemesine kavraması, daha sağlam ve sürdürülebilir uygulamalar oluşturmasına yardımcı olacaktır.

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

Gönder

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.
Mobil sürümden çık