Callback fonksiyon, başka bir fonksiyona argüman olarak verilen ve o fonksiyon tarafından uygun bir anda çağrılan fonksiyondur. [1, 2].map((x) => x * 2) içindeki arrow function bir callback'tir; setTimeout(() => ..., 1000) içindeki de. İlki senkron çalışır, yani map bitmeden hemen çağrılır. İkincisi asenkron çalışır, yani mevcut kod tamamlandıktan sonra, ileri bir zamanda çağrılır. Bu ayrımı bilmek, kodunuzun hangi sırayla çalışacağını doğru tahmin etmenin temelidir.

Bu yazıda callback'in ne olduğunu, senkron ve asenkron callback farkını, iç içe callback'lerin yol açtığı "callback hell" sorununu ve bu sorunu Promise ile nasıl çözebileceğinizi adım adım gösteriyoruz. Tüm örnekler Node.js 24 ile çalıştırıldı.

Callback nedir?

Callback nedir?

JavaScript'te fonksiyonlar birinci sınıf değerlerdir: Bir değişkene atanabilir, argüman olarak verilebilir, başka bir fonksiyondan döndürülebilir. Callback bu özelliğin doğrudan sonucudur. Aşağıda hesapla fonksiyonu, ne yapacağını bilmeden işlemi kendisine verilen callback'e bırakıyor:

javascript
function hesapla(a, b, islem) {
  return islem(a, b);
}

const topla = (x, y) => x + y;
console.log(hesapla(4, 6, topla)); // 10
console.log(hesapla(4, 6, (x, y) => x * y)); // 24

Callback'i verirken fonksiyonun adını parantezsiz yazdığınıza dikkat edin: hesapla(4, 6, topla). topla() yazsaydınız fonksiyonu hemen çalıştırır ve sonucunu (NaN) argüman olarak vermiş olurdunuz. Callback'leri genellikle arrow function olarak yazarız; neden tercih edildiğini fonksiyon türleri yazısında anlatıyoruz.

Senkron callback: hemen çalışır

Dizi metotları (map, filter, forEach, sort, reduce) callback'lerini senkron çağırır. Metot dönmeden önce callback her öğe için çalışmış olur:

javascript
console.log("1. başla");
[10, 20].forEach((n) => console.log("2. senkron callback:", n));
console.log("3. bitir");
// 1. başla
// 2. senkron callback: 10
// 2. senkron callback: 20
// 3. bitir
javascript
const sayilar = [3, 1, 2];
console.log(sayilar.map((n) => n * 10)); // [ 30, 10, 20 ]
console.log(sayilar.filter((n) => n > 1)); // [ 3, 2 ]
console.log([...sayilar].sort((a, b) => a - b)); // [ 1, 2, 3 ]

Dizi metotlarının tamamı ve callback'lerinin aldığı parametreler için map, filter ve reduce yazısına bakabilirsiniz.

Asenkron callback: sonra çalışır

Asenkron callback: sonra çalışır: Ne zaman çalışır, Örnek API, Sonucu nasıl alırsınız, try/catch ile hata yakalama

Zamanlayıcılar, ağ ve dosya işlemleri gibi API'ler callback'i hemen değil, iş tamamlandığında çağırır. Gecikme 0 olsa bile setTimeout callback'i, o an çalışan kod bitmeden çalışmaz:

javascript
console.log("1. başla");
setTimeout(() => console.log("3. asenkron callback"), 0);
console.log("2. bitir");
// 1. başla
// 2. bitir
// 3. asenkron callback

Bunun nedeni JavaScript'in "tamamlanana kadar çalıştır" (run-to-completion) modelidir: Bir iş başladıktan sonra bitene kadar başka bir callback araya giremez. Zamanlayıcının callback'i bir kuyrukta bekler ve sıra geldiğinde çalışır. Kuyrukların ve olay döngüsünün nasıl işlediğini event loop yazısında ayrıntılı anlatıyoruz.

ÖzellikSenkron callbackAsenkron callback
Ne zaman çalışırÇağıran fonksiyon dönmedenMevcut kod bittikten sonra
Örnek APImap, filter, forEach, sortsetTimeout, Promise then, olay dinleyicileri
Sonucu nasıl alırsınızDönüş değeriyleCallback içinde ya da Promise ile
try/catch ile hata yakalamaÇalışırDıştaki try/catch yakalamaz

Başarı ve hata için ayrı callback'ler

Promise'ten önce asenkron API'ler genellikle iki callback alırdı: biri işlem başarılı olunca, diğeri hata olunca çağrılırdı. MDN'nin Promise rehberi de anlatıma bu tür bir API ile başlar. Aşağıda böyle bir API'yi zamanlayıcıyla taklit ediyoruz:

javascript
function kullaniciGetir(id, basarili, hatali) {
  setTimeout(() => {
    if (id <= 0) {
      hatali(new Error("Geçersiz id"));
    } else {
      basarili({ id, ad: "Deniz" });
    }
  }, 10);
}

kullaniciGetir(
  7,
  (k) => console.log("Bulundu:", k.ad), // Bulundu: Deniz
  (h) => console.log("Hata:", h.message),
);
kullaniciGetir(
  0,
  (k) => console.log("Bulundu:", k.ad),
  (h) => console.log("Hata:", h.message), // Hata: Geçersiz id
);

Callback hell: iç içe geçen adımlar

Birbirine bağlı birkaç asenkron adımı sırayla çalıştırmak istediğinizde, her adım bir öncekinin callback'inin içine yazılır. Adım sayısı arttıkça kod sağa doğru kayar, hata yönetimi her seviyede tekrarlanır ve akışı okumak zorlaşır. Buna "callback hell" denir:

javascript
function adim(ad, girdi, callback) {
  setTimeout(() => callback(`${girdi} > ${ad}`), 10);
}

adim("sipariş", "başla", (s1) => {
  adim("ödeme", s1, (s2) => {
    adim("kargo", s2, (s3) => {
      console.log(s3); // başla > sipariş > ödeme > kargo
    });
  });
});

Bu örnekte hata yönetimi bile yok. Gerçek kodda her seviyeye bir hata callback'i daha eklenir. Ayrıca asenkron callback içinde fırlatılan bir hatayı, çağrıyı saran try/catch yakalayamaz, çünkü callback çalıştığında try bloğu çoktan bitmiştir:

javascript
try {
  setTimeout(() => {
    throw new Error("callback içinde hata");
  }, 0);
} catch (e) {
  console.log("Yakalandı:", e.message); // hiç çalışmaz
}
process.on("uncaughtException", (e) => console.log("try/catch kaçırdı:", e.message));
// try/catch kaçırdı: callback içinde hata

Son satırdaki process.on yalnızca Node.js'e özgüdür ve burada hatanın dıştaki try/catch'ten kaçtığını göstermek için kullanıldı. MDN de setTimeout callback'inde oluşan hatanın hiçbir şey tarafından yakalanmadığını, bunun API'nin tasarımından kaynaklandığını belirtir. Hata yönetiminin genel kuralları için try/catch yazısına bakabilirsiniz.

Promise'e geçiş: zinciri düzleştirmek

Promise, asenkron bir işlemin gelecekteki sonucunu temsil eden bir nesnedir. Callback'i fonksiyona vermek yerine, fonksiyonun döndürdüğü Promise'e then() ile eklersiniz. then() her seferinde yeni bir Promise döndürdüğü için adımlar iç içe değil, alt alta yazılır ve tek bir catch() tüm zincirin hatalarını yakalar. İlk iş, callback alan eski API'yi en alt seviyede bir kez sarmalamaktır:

javascript
const adim = (ad, girdi) =>
  new Promise((resolve) => setTimeout(() => resolve(`${girdi} > ${ad}`), 10));

adim("sipariş", "başla")
  .then((s1) => adim("ödeme", s1))
  .then((s2) => adim("kargo", s2))
  .then((s3) => console.log(s3)) // başla > sipariş > ödeme > kargo
  .catch((hata) => console.log("Hata:", hata.message));

MDN'nin önerisi de budur: Callback alan fonksiyonları mümkün olan en alt seviyede Promise ile sarın ve sonra onları bir daha doğrudan çağırmayın. Basit bir bekleme yardımcısı şu kadar kısadır:

javascript
const bekle = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

bekle(20).then(() => console.log("20 ms geçti")); // 20 ms geçti

Aynı akış async/await ile senkron koda benzer şekilde yazılabilir. Bu sözdizimi Promise'lerin üzerine kuruludur:

javascript
const adim = (ad, girdi) =>
  new Promise((resolve) => setTimeout(() => resolve(`${girdi} > ${ad}`), 10));

async function surec() {
  const s1 = await adim("sipariş", "başla");
  const s2 = await adim("ödeme", s1);
  const s3 = await adim("kargo", s2);
  console.log(s3); // başla > sipariş > ödeme > kargo
}
surec();

await kullanımı, hata yakalama ve paralel çalıştırma için async/await rehberine ve Promise.all, allSettled, race ve any yazısına göz atın.

Promise callback'leri her zaman asenkron çalışır

MDN, bir callback'in bazen senkron bazen asenkron çağrıldığı API tasarımından kaçınılması gerektiğini vurgular; bu durumda çağıran kod hangi sırayla çalışacağını bilemez. Promise bu belirsizliği ortadan kaldırır: then() ile eklenen callback, Promise zaten çözülmüş olsa bile hiçbir zaman senkron çağrılmaz, mevcut kod bittikten sonra çalışır:

javascript
Promise.resolve().then(() => console.log(2));
console.log(1);
// 1
// 2

Sık yapılan hatalar

Sık yapılan hatalar: Callback'i verirken fonksiyonu çağırmak, Asenkron callback'in sonucunu bir sonraki satırda kullanmaya çalışmak; o satır..., Asenkron callback içindeki hatayı...
  • Callback'i verirken fonksiyonu çağırmak: setTimeout(calis(), 100) yerine setTimeout(calis, 100) yazılmalı.
  • Asenkron callback'in sonucunu bir sonraki satırda kullanmaya çalışmak; o satır çalıştığında callback henüz çağrılmamıştır.
  • Asenkron callback içindeki hatayı dıştaki try/catch ile yakalamayı beklemek.
  • Metodu callback olarak verip this değerinin korunacağını sanmak; ayrıntılar this yazısında.
  • then() içinde Promise döndürmeyi unutmak ve zinciri koparmak.
  • Aynı callback'i bazen senkron bazen asenkron çağıran bir API yazmak.

Sık Sorulan Sorular

Her callback asenkron mudur?

Hayır. Callback yalnızca "başka bir fonksiyona verilen fonksiyon" demektir. Dizi metotlarının callback'leri senkron, zamanlayıcıların ve Promise then() callback'lerinin her zaman asenkron çalışır.

Callback hell nasıl önlenir?

En kalıcı çözüm, callback alan API'leri Promise ile sarmalayıp zincir ya da async/await kullanmaktır. Kısa vadede iç içe anonim fonksiyonlar yerine adlandırılmış fonksiyonlar kullanmak da okunurluğu artırır.

Promise callback'lerden tamamen kurtarır mı?

Hayır, Promise de callback kullanır; then() ve catch()'e verdiğiniz fonksiyonlar callback'tir. Fark, callback'in fonksiyona verilmek yerine dönen nesneye eklenmesi ve zamanlama ile hata yayılımı konusunda dilin güvence vermesidir.

setTimeout(fn, 0) neden hemen çalışmıyor?

Çünkü callback bir kuyruğa eklenir ve ancak o an çalışan kod tamamlandığında sırası gelir. Promise callback'leri ise mikro görev (microtask) kuyruğunda bekler ve zamanlayıcı callback'lerinden önce çalışır; ayrıntılar event loop yazısında.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: JavaScript öğrenme rehberi · Promise nedir · async/await · Event loop · Dizi metotları · Node.js nedir