Mert Özen Her Satırda Daha İleri
Frontend JavaScript Orta — #09

JavaScript Promise: Üç Durum, then/catch/finally Zinciri ve Hata Yayılımı

Mert Özen 3 Kas 2026 8 dk 4 görüntülenme
JavaScript Promise: Üç Durum, then/catch/finally Zinciri ve Hata Yayılımı

Promise'in üç durumunu çağrı cihazı benzetmesiyle kuruyor; new Promise, then/catch/finally, zincirleme ve hata yayılımını, Promise.all ile race'i ve callback'i promise'e çevirmeyi örneklerle görüyoruz.

Sürüm notu: Bu yazıdaki kodlar ES2026 ve güncel tarayıcılar (Chrome, Edge, Firefox, Safari) içindir.

Önce Tahmin Et

Bir promise oluşturuyoruz, içinde iki console.log ve bir resolve var. Dışarıda da bir then ve bir console.log. Rakamlar hangi sırayla yazılır?

const p = new Promise((resolve) => {
  console.log("1");
  resolve("2");
  console.log("3");
});

p.then((value) => console.log(value));
console.log("4");

Okumaya devam etmeden önce bir tahmin yap. Bir önceki yazıdaki event loop resmini hatırlıyorsan bir avantajın var, ama bu sorunun içinde küçük bir sürpriz daha saklı. Cevabı promise'in nasıl oluşturulduğunu gördükten sonra açıklayacağız.

Promise Nedir? Elindeki Çağrı Cihazı

Kalabalık bir hamburgercide siparişini verdiğinde sana genelde küçük, titreşen bir çağrı cihazı verirler. Hamburgerin henüz hazır değil ama elinde, hazır olduğunda haber verecek bir şey var. Masana geçip telefonuna bakabilir, arkadaşınla konuşabilirsin. Cihaz titrediğinde ya yemeğin hazırdır ya da "üzgünüz, malzeme bitti" haberi gelir.

Promise tam olarak bu cihazdır: henüz elinde olmayan, ileride gelecek bir sonucu temsil eden bir object. ES2015 ile dile girdi. Bir önceki yazıda gördüğümüz callback deseninde sonucu almak için fonksiyonuna bir callback verirdin; promise'te ise fonksiyon sana bir object döndürür ve sonuçla ne yapacağına sen karar verirsin. Kontrol yeniden sende.

Her promise şu üç durumdan birindedir:

pending (bekliyor): iş henüz bitmedi. Cihaz elinde, titremedi.

fulfilled (yerine getirildi): iş başarıyla bitti ve bir değer üretti. Hamburger hazır.

rejected (reddedildi): iş başarısız oldu ve bir sebep, genelde bir Error üretti. Malzeme bitmiş.

Bir promise pending'den çıkıp diğer iki durumdan birine geçtiğinde artık "sonuçlanmış" (settled) sayılır ve bir daha asla değişmez. Hazır olduğu söylenen hamburger sonradan "aslında bitmişti" olamaz.

new Promise ile Kendi Promise'ini Üretmek

Çoğu zaman promise'i sen üretmezsin; fetch gibi tarayıcı API'leri sana hazır bir promise verir. Ama nasıl çalıştığını anlamak için bir tane elle yapalım. Bunu tarayıcının konsoluna yapıştırıp dene:

function wait(ms) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(`${ms} ms passed`), ms);
  });
}

wait(1000).then((message) => console.log(message)); // 1 saniye sonra: "1000 ms passed"

new Promise'e verdiğin fonksiyona executor denir. JavaScript engine'i ona iki fonksiyon uzatır: resolve ve reject. İş başarıyla bitince resolve(value), başarısız olunca reject(error) çağırırsın:

function rollDice() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const value = Math.floor(Math.random() * 6) + 1;
      if (value === 1) {
        reject(new Error("You rolled a 1, you lose"));
      } else {
        resolve(value);
      }
    }, 500);
  });
}

rollDice()
  .then((value) => console.log(`Dice: ${value}`))
  .catch((error) => console.error(error.message));

Birkaç kez çalıştır; bazen sonucu, bazen hatayı görürsün. reject'e her zaman bir Error object'i vermeye alış. Düz bir string de verebilirsin ama o zaman stack trace'i kaybedersin ve daha önce gördüğümüz instanceof Error kontrolleri çalışmaz.

Şimdi baştaki soruya bakalım. Executor, new Promise çağrıldığı anda, senkron olarak çalışır. Yani önce 1 yazılır, resolve("2") promise'i hemen fulfilled yapar ama executor durmaz, 3 de yazılır. Ardından then kaydedilir ve 4 yazılır. then'e verdiğin fonksiyon ise, promise zaten hazır olsa bile hemen çalışmaz; o an çalışan kod bitince sırası gelir. Cevap: 1, 3, 4, 2.

Bu davranışa dikkat et: resolve çağrısı executor'ı bitirmez ve bir promise'i bir kez sonuçlandırdıktan sonra yapılan ikinci bir resolve ya da reject çağrısı sessizce yok sayılır.

then, catch ve finally

Bir promise'in sonucuyla ilgilenmek için üç method var. then(fn) başarı durumunda çalışır ve değeri alır. catch(fn) hata durumunda çalışır ve hatayı alır. ES2018 ile gelen finally(fn) ise sonuç ne olursa olsun çalışır ve argüman almaz; yükleniyor göstergesini kapatmak gibi temizlik işleri için idealdir:

console.log("Loading...");

rollDice()
  .then((value) => console.log(`Dice: ${value}`))
  .catch((error) => console.error(error.message))
  .finally(() => console.log("Loading finished"));

then'in callback'lerinin her zaman o an çalışan kod bittikten sonra çalıştığını gördük. Bir ayrıntı daha: Promise.resolve().then(...) ile setTimeout(..., 0) yan yana olduğunda promise'in callback'i önce çalışır. Promise'lerin kendine ait, daha öncelikli bir queue'yu var. Bu queue'nun adını ve tam olarak ne zaman boşaltıldığını İleri serideki event loop yazısında göreceğiz; şimdilik "promise callback'leri zamanlayıcılardan önce gelir" bilgisi yeterli.

Zincirleme: then Yeni Bir Promise Döndürür

Promise'lerin asıl gücü burada. then her çağrıldığında yeni bir promise döndürür. Callback'inden ne döndürürsen, bu yeni promise o değerle sonuçlanır. Eğer döndürdüğün şey kendisi bir promise ise, zincir onun bitmesini bekler.

wait(500)
  .then(() => 2)
  .then((n) => n * 10)
  .then((n) => wait(500).then(() => n + 1))
  .then((n) => console.log(n)); // yaklaşık 1 saniye sonra 21

Bir önceki yazıdaki callback piramidini hatırla. Aynı fonksiyonların promise döndüren versiyonlarını yazarsak piramit düz bir zincire dönüşür:

function getUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id <= 0) reject(new Error("Invalid user id"));
      else resolve({ id, name: "Ellie" });
    }, 500);
  });
}

function getOrders(userId) {
  return wait(500).then(() => [{ id: 101 }, { id: 102 }]);
}

function getOrderDetail(orderId) {
  return wait(500).then(() => ({ id: orderId, total: 349.9 }));
}

getUser(1)
  .then((user) => getOrders(user.id))
  .then((orders) => getOrderDetail(orders[0].id))
  .then((detail) => console.log(`First order: ${detail.total}`))
  .catch((error) => console.error(error.message));

Kod artık sağa değil aşağı doğru akıyor ve her adım bir öncekinin sonucunu alıyor. En güzeli de en sondaki tek catch: zincirin herhangi bir yerindeki hata oraya düşüyor.

Hata Yayılımı

Bir zincirde hata çıktığında, ister reject ile ister bir then içinde throw ile, promise rejected olur ve zincir aradaki tüm then'leri atlayıp ilk catch'e kadar ilerler. Daha önce gördüğümüz try/catch'in promise versiyonu gibi:

wait(300)
  .then(() => {
    throw new Error("Something went wrong in step two");
  })
  .then(() => console.log("This line never runs"))
  .catch((error) => {
    console.error("Caught:", error.message);
    return "default value";
  })
  .then((value) => console.log("The chain carries on:", value));

Son satıra dikkat et. catch da bir promise döndürür ve hatayı yakalayıp normal bir değer döndürdüğünde zincir kaldığı yerden başarıyla devam eder. Yani catch, hatayı "çözdüm" diyebileceğin bir nokta. Eğer orada hatayı çözemiyorsan, tekrar throw ederek bir sonraki catch'e iletebilirsin.

Peki hiçbir catch yoksa? Reddedilen bir promise'i kimse yakalamazsa tarayıcı konsolda "Uncaught (in promise)" diye bir hata gösterir ve window üzerinde unhandledrejection event'i tetiklenir. Hata yönetimi yazısında global hata yakalamadan söz ederken bu event'e işaret etmiştik.

Promise.all ve Promise.race'e İlk Bakış

Birbirinden bağımsız birkaç işi aynı anda başlatıp hepsinin bitmesini beklemek istediğinde Promise.all kullanırsın. Bir array promise alır ve sonuçların array'ini, aynı sırayla döndüren tek bir promise verir:

Promise.all([wait(1000), wait(500), wait(200)])
  .then((results) => console.log(results));
// yaklaşık 1 saniye sonra: ["1000 ms passed", "500 ms passed", "200 ms passed"]

Üç iş paralel yürüdüğü için toplam süre 1700 değil, en uzunu kadar, yani bir saniye. Bir uyarı: promise'lerden biri reddedilirse Promise.all diğerlerini beklemeden hemen reddedilir.

Promise.race ise ilk sonuçlanan promise'in sonucunu alır. Klasik kullanımı bir zaman aşımı kurmaktır:

function timeout(ms) {
  return new Promise((_, reject) => {
    setTimeout(() => reject(new Error("Timed out")), ms);
  });
}

Promise.race([wait(2000), timeout(1000)])
  .then((message) => console.log(message))
  .catch((error) => console.error(error.message)); // "Timed out"

Bunların yanında Promise.allSettled ve Promise.any da var. Aralarındaki farkları ve ne zaman hangisini seçeceğini İleri serideki asenkron yazısında karşılaştıracağız.

Callback'i Promise'e Çevirmek

Elinde callback ile çalışan eski bir fonksiyon varsa, onu promise döndüren bir fonksiyonla sarabilirsin. Hata-önce callback geleneğini izleyen fonksiyonlar için kalıp hep aynıdır:

// Eski, callback'li API
function loadSettings(callback) {
  setTimeout(() => callback(null, { theme: "dark" }), 300);
}

// Promise sarmalayıcısı
function loadSettingsAsync() {
  return new Promise((resolve, reject) => {
    loadSettings((error, settings) => {
      if (error) reject(error);
      else resolve(settings);
    });
  });
}

loadSettingsAsync().then((settings) => console.log(settings.theme)); // "dark"

Callback'in hata kolunu reject'e, başarı kolunu resolve'a bağladık. Artık bu fonksiyonu diğer promise'lerle zincirleyebilir, Promise.all'a verebilirsin. new Promise'i elle yazmanın en meşru yeri tam olarak burası: promise bilmeyen bir API'yi promise dünyasına taşımak.

Sık Düşülen Tuzaklar

Birinci ve en sinsi tuzak, then içinde return'ü unutmak:

getUser(1)
  .then((user) => {
    getOrders(user.id); // return yok!
  })
  .then((orders) => console.log(orders)); // undefined

Siparişler istenmiş ama promise'i kimse döndürmemiş. Zincir onu beklemez, bir sonraki then hemen undefined ile çalışır. Siparişler sonra gelir ama kimse onları dinlemiyordur. Kural basit: then içinde bir promise başlattıysan onu döndür.

İkincisi, then'leri iç içe yazıp piramidi geri getirmek. getUser(1).then((user) => getOrders(user.id).then(...)) gibi bir yapı çalışır, ama zincirlemenin sağladığı düzlüğü çöpe atar. Bir önceki adımın değerine sonraki adımlarda da ihtiyacın olduğunda iç içe yazmak cazip gelir; bunun temiz çözümü bir sonraki yazının konusu.

Üçüncüsü, zincirin sonuna catch koymamak. Hata sessizce kaybolmaz ama kullanıcıya bir şey gösteremezsin; sadece konsolda kırmızı bir satır kalır.

Dördüncüsü, zaten promise döndüren bir şeyi gereksiz yere new Promise ile sarmak. new Promise((resolve) => resolve(fetch(url))) yazmak yerine doğrudan fetch(url)'i döndür. Gereksiz sarmalama, hatanın yolunu kaybetmesine kolayca sebep olur.

Küçük Bir Deneme

★☆☆ wait fonksiyonunu kullanarak 1, 2 ve 3 saniye sonra sırayla "One", "Two", "Three" yazan bir zincir kur. Tek bir zincir olsun, iç içe then olmasın.

★★☆ Çıktıyı ve sırasını tahmin et, sonra dene:

setTimeout(() => console.log("A"), 0);

Promise.resolve("B")
  .then((value) => {
    console.log(value);
    throw new Error("C");
  })
  .catch((error) => console.log(error.message))
  .finally(() => console.log("D"));

console.log("E");

★★★ getUser, getOrders ve getOrderDetail zincirinin sonunda kullanıcının adını da yazdırmak istiyorsun: "Ellie paid 349.9 on their first order". Ama zincirin son adımında elinde sadece detail var. İç içe then yazmadan bu sorunu nasıl çözersin? Birkaç yol dene.

Zincirler piramitten çok daha iyi ama hâlâ bir sürü then ve arrow function var, son alıştırmadaki gibi değerleri adımlar arasında taşımak da biraz zahmetli. Bir sonraki yazıda aynı promise'leri senkron kod gibi okunan bir syntax'la yazmamızı sağlayan async/await'e geçeceğiz.