AsyncLocalStorage, bir değeri (genellikle istek kimliği, kullanıcı bilgisi ya da dil tercihi gibi bir bağlam nesnesini) asenkron bir işlem zinciri boyunca, fonksiyonlara parametre olarak taşımadan erişilebilir kılan Node.js sınıfıdır. node:async_hooks modülünden gelir: als.run(store, fn) ile bir bağlam açarsınız, bu bağlam içinde başlatılan her await, zamanlayıcı ve geri çağırma als.getStore() ile aynı değeri okur. Belgeler bunu diğer dillerdeki thread-local storage'a benzetir ve sınıf Stability: 2 - Stable olarak işaretlidir.

Bu yazıda run() ve getStore() kurallarını, bir HTTP sunucusunda istek kimliğiyle log yazmayı, bağlamın kaybolduğu durumları ve AsyncResource ile AsyncLocalStorage.bind() / snapshot() ile nasıl düzeltileceğini, deneysel API'leri ve aynı modüldeki düşük seviyeli async_hooks API'sinin neden önerilmediğini Node.js v26.13.0 üzerinde çalıştırılmış örneklerle anlatıyoruz. Serinin tamamı için Node.js öğrenme rehberi sayfasına bakabilirsiniz.

Hangi sorunu çözer?

Hangi sorunu çözer?

Bir web isteği işlenirken onlarca fonksiyon çağrılır: doğrulama, veritabanı sorgusu, harici bir servise istek, log yazma. Her log satırına o isteğin kimliğini eklemek isterseniz, en basit yol kimliği her fonksiyona parametre olarak geçirmektir. Bu, ilgisi olmayan fonksiyonların imzalarını kirletir ve üçüncü taraf kodlardan geçerken kopar. Global bir değişken de çözüm olmaz: Node.js aynı anda birçok isteği işlediği için bir isteğin await ile beklediği sırada başka bir istek değişkenin üzerine yazar.

AsyncLocalStorage her asenkron zincire ayrı bir "depo" (store) bağlar. Belgelere göre her AsyncLocalStorage örneği bağımsız bir bağlam tutar; aynı anda birden çok örnek, birbirinin verisine karışmadan var olabilir. Belgeler ayrıca kendi uygulamanızı node:async_hooks üzerine kurabileceğinizi, ancak AsyncLocalStorage'ın performanslı ve bellek açısından güvenli, kendiniz yazmanız zor optimizasyonlar içeren bir uygulama olduğu için tercih edilmesi gerektiğini söyler.

Temel kullanım: istek kimliğiyle log yazmak

Aşağıdaki sunucu her isteğe sıralı bir kimlik verip run() içinde işliyor. log() fonksiyonu kimliği parametre olarak almıyor; getStore() ile okuyor. İki istek bilerek iç içe geçecek şekilde gönderildi: önce yavaş olan başlıyor, o veritabanını beklerken hızlı olan başlayıp bitiyor. Örnek, sunucuyu kendi içinden fetch ile çağırıp sonunda kapatıyor:

javascript
import http from 'node:http';
import { AsyncLocalStorage } from 'node:async_hooks';
import { setTimeout as bekle } from 'node:timers/promises';

const istekBaglami = new AsyncLocalStorage();
let sira = 0;

function log(mesaj) {
  const id = istekBaglami.getStore()?.id ?? '-';
  console.log(`[istek ${id}] ${mesaj}`);
}

async function kullaniciBul(gecikme) {
  await bekle(gecikme);           // veritabanı sorgusu yerine
  log('kullanıcı bulundu');
}

const sunucu = http.createServer((req, res) => {
  istekBaglami.run({ id: ++sira }, async () => {
    log(`başladı ${req.url}`);
    await kullaniciBul(req.url === '/yavas' ? 60 : 5);
    log('yanıt gönderiliyor');
    res.end('ok');
  });
});

await new Promise((r) => sunucu.listen(0, r));
const adres = `http://localhost:${sunucu.address().port}`;
const yavas = fetch(`${adres}/yavas`);
await bekle(15);
await Promise.all([yavas, fetch(`${adres}/hizli`)]);
log('tüm istekler bitti');
sunucu.close();
// çıktı: [istek 1] başladı /yavas
// çıktı: [istek 2] başladı /hizli
// çıktı: [istek 2] kullanıcı bulundu
// çıktı: [istek 2] yanıt gönderiliyor
// çıktı: [istek 1] kullanıcı bulundu
// çıktı: [istek 1] yanıt gönderiliyor
// çıktı: [istek -] tüm istekler bitti

Satırlar karışık sırada geldiği hâlde her biri doğru kimliği taşıyor. kullaniciBul() içindeki await'ten sonra çalışan kod, kendisini başlatan isteğin deposunu görüyor. Son satır ise hiçbir run() içinde olmadığı için depo undefined ve log - yazıyor. Aynı fikir, Express middleware zincirlerinde istek bağlamını taşımak için de kullanılabilir. Node.js'te çatı kullanmadan sunucu yazmak için node:http yazısına bakabilirsiniz.

run() ve getStore() kuralları

asyncLocalStorage.run(store, callback[, ...args]) fonksiyonu callback'i senkron olarak bir bağlam içinde çalıştırır ve callback'in dönüş değerini döndürür. Depo, callback'in dışında erişilebilir değildir; ancak callback içinde oluşturulan tüm asenkron işlemler depoya erişebilir. İç içe run() çağrıları yeni bir depo açar, iç çağrı bitince dıştaki depo geri gelir. getStore(), bir run() veya enterWith() ile başlatılmış bağlamın dışında çağrılırsa undefined döndürür.

javascript
import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();

const sonuc = als.run('dış', () => {
  console.log('1:', als.getStore());
  als.run('iç', () => console.log('2:', als.getStore()));
  console.log('3:', als.getStore());
  setTimeout(() => console.log('5 (zamanlayıcı):', als.getStore()), 5);
  return 'run dönüş değeri';
});

console.log('4 (run dışında):', als.getStore(), '|', sonuc);
// çıktı: 1: dış
// çıktı: 2: iç
// çıktı: 3: dış
// çıktı: 4 (run dışında): undefined | run dönüş değeri
// çıktı: 5 (zamanlayıcı): dış

Zamanlayıcı, run() çoktan bittikten sonra çalıştığı hâlde dış değerini görüyor; çünkü run() içinde kuruldu. Callback hata fırlatırsa belgelere göre hatayı run() da fırlatır ve bağlamdan çıkılır, yani catch bloğunda depo artık yoktur:

javascript
import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();
try {
  als.run({ id: 7 }, () => {
    throw new Error('işleyicide hata');
  });
} catch (err) {
  console.log(err.message, '| catch içinde store:', als.getStore());
}
// çıktı: işleyicide hata | catch içinde store: undefined

Async fonksiyonlarla kullanırken belgelerin önerdiği desen, run()'ın döndürdüğü Promise'i beklemektir: await als.run(new Map(), () => foo()). Depo yalnızca callback'in ve onun çağırdığı fonksiyonların içinde görünür. Async fonksiyonların nasıl çalıştığı için async/await yazısına bakın.

Depoda ne saklanmalı?

Depo herhangi bir değer olabilir: sayı, string ya da nesne. Belgelerdeki async/await örneği her istek için yeni bir Map verir ve bağlam içinde als.getStore().set('key', value) ile değer ekler. Bu desen, isteğin ilerleyen adımlarında depoya yeni bilgi (doğrulanmış kullanıcı, başlangıç zamanı, izleme kimliği) eklemenize izin verir. Dikkat edilmesi gereken nokta, deponun o isteğe ait olmasıdır: her run() çağrısına yeni bir nesne verin ve depoyu istekler arasında paylaşılan bir önbellek gibi kullanmayın. Depo, onu kullanan asenkron kaynaklarla birlikte çöp toplanır; belgelere göre disable() çağırma gereği AsyncLocalStorage örneğinin kendisi içindir, depolar için değil. API tasarımında istek bağlamının nasıl kullanılabileceğine dair genel bir örnek için Node.js REST API yazısına bakabilirsiniz.

defaultValue ve name seçenekleri

YAML kaydına göre v24.0.0 ile yapıcıya defaultValue ve name seçenekleri eklendi. defaultValue, depo verilmediğinde kullanılacak değerdir; name ise örneğe bir ad verir ve asyncLocalStorage.name ile okunur. Seçenek verilmeyen bir örnekte bağlam dışındaki getStore() yine undefined döner:

javascript
import { AsyncLocalStorage } from 'node:async_hooks';

const dil = new AsyncLocalStorage({ defaultValue: 'tr', name: 'dil' });
console.log(dil.name, dil.getStore());
dil.run('en', () => console.log(dil.getStore()));

const bos = new AsyncLocalStorage();
console.log(bos.getStore());
// çıktı: dil tr
// çıktı: en
// çıktı: undefined

Bağlam kaybı ve çözümleri

Bağlam kaybı ve çözümleri: AsyncLocalStorage.bind(fn), AsyncLocalStorage.snapshot(), AsyncResource.bind(fn), asyncResource.runInAsyncScope(fn)

Belgeler, çoğu durumda AsyncLocalStorage'ın sorunsuz çalıştığını ama nadiren bir asenkron işlemde deponun kaybolabileceğini belirtir. Tipik neden, geri çağırmaları kendi kuyruğunda biriktirip daha sonra başka bir bağlamdan çalıştıran kodlardır: bağlantı havuzları, toplu işleyiciler ya da özel thenable uygulamaları. Aşağıda kendi kuyruğumuza attığımız düz bir callback depoyu göremiyor; AsyncLocalStorage.bind() ile sarılan ise görüyor:

javascript
import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();
const bekleyenler = [];

function sonraCalistir(fn) { bekleyenler.push(fn); }   // kendi kuyruğumuz
function kuyruguBosalt() { for (const fn of bekleyenler.splice(0)) fn(); }

als.run({ kullanici: 'ayse' }, () => {
  sonraCalistir(() => console.log('düz callback:', als.getStore()));
  sonraCalistir(AsyncLocalStorage.bind(() => console.log('bind ile:', als.getStore())));
});

kuyruguBosalt();
// çıktı: düz callback: undefined
// çıktı: bind ile: { kullanici: 'ayse' }

Belgelerin önerdiği sorun giderme yöntemi şudur: callback tabanlı kodu util.promisify() ile Promise'e çevirmek çoğu zaman yeterlidir. Callback tabanlı bir API kullanmak zorundaysanız ya da kod özel bir thenable varsayıyorsa, asenkron işlemi doğru bağlamla ilişkilendirmek için AsyncResource sınıfını kullanın. Kaybın yerini bulmak için şüphelendiğiniz çağrılardan sonra getStore() değerini loglayın; undefined görmeye başladığınız yerdeki son callback büyük olasılıkla sorumludur. promisify için util.promisify yazısına bakabilirsiniz.

AraçNe yaparKararlılık
AsyncLocalStorage.bind(fn)fn fonksiyonunu o anki yürütme bağlamına bağlarStable (v23.11.0 / v22.15.0)
AsyncLocalStorage.snapshot()O anki bağlamı yakalayan bir çalıştırıcı fonksiyon döndürürStable (v23.11.0 / v22.15.0)
AsyncResource.bind(fn)fn fonksiyonunu o anki bağlama bağlar (async_hooks kaynağıyla)Stable
asyncResource.runInAsyncScope(fn)fn fonksiyonunu kaynağın bağlamında çalıştırırStable

snapshot(): bağlamı yakalayıp sonra kullanmak

AsyncLocalStorage.snapshot() o anki yürütme bağlamını yakalar ve bir fonksiyon döndürür. Bu fonksiyona hangi fonksiyonu verirseniz, yakalanan bağlam içinde çalıştırılır. Belgelere göre basit bağlam izleme ihtiyaçlarında AsyncResource yerine kullanılabilir; örneğin bir sınıf örneği oluşturulduğu andaki bağlamı saklayıp sonraki metot çağrılarında kullanabilir:

javascript
import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();
const calistir = als.run(123, () => AsyncLocalStorage.snapshot());
console.log(als.run(321, () => calistir(() => als.getStore())));
// çıktı: 123

EventEmitter dinleyicileri hangi bağlamda çalışır?

Belgeler önemli bir ayrıntıya dikkat çeker: bir EventEmitter dinleyicisi, on() çağrıldığı andaki bağlamda değil, olayı yayan kodun bağlamında çalışabilir. Dinleyiciyi kayıt anındaki bağlama bağlamak isterseniz AsyncResource.bind() ile sarmalarsınız. Aynı yaklaşım stream'ler ve benzeri olay tabanlı sınıflar için de geçerlidir:

javascript
import { EventEmitter } from 'node:events';
import { AsyncLocalStorage, AsyncResource } from 'node:async_hooks';

const als = new AsyncLocalStorage();
const yayin = new EventEmitter();

als.run('abone olan istek', () => {
  yayin.on('mesaj', () => console.log('düz dinleyici:', als.getStore()));
  yayin.on('mesaj', AsyncResource.bind(() => console.log('bind dinleyici:', als.getStore())));
});

als.run('yayan istek', () => yayin.emit('mesaj'));
// çıktı: düz dinleyici: yayan istek
// çıktı: bind dinleyici: abone olan istek

Hangisinin doğru olduğu ihtiyaca bağlıdır. Örneğin birçok isteğin paylaştığı bir önbellek olayı yayıyorsa, log satırında olayı tetikleyen isteğin mi yoksa abone olan isteğin mi kimliğini görmek istediğinize karar vermeniz gerekir. EventEmitter'ın kendisi için EventEmitter yazısına bakın.

AsyncResource: kendi asenkron kaynağınızı tanımlamak

AsyncResource, kendi asenkron kaynaklarını yazan kütüphanelerin genişletmesi için tasarlanmış bir sınıftır. Bir veritabanı istemcisi veya worker havuzu gibi, iş alıp sonucu daha sonra geri çağıran yapılarda, callback'i this.runInAsyncScope(callback, null, err, data) ile çağırmak, callback'in işi isteyen kodun bağlamında çalışmasını sağlar. Belgeler worker havuzu ve EventEmitter entegrasyonu için ayrıntılı örnekler içerir. emitDestroy() metodu yalnızca bir kez çağrılmalıdır; birden fazla çağrıda hata fırlatılır. Thread havuzları için worker_threads yazısına göz atabilirsiniz.

Deneysel API'ler: enterWith, exit, disable, withScope

Deneysel API'ler: enterWith, exit, disable, withScope: enterWith(store), exit(callback), disable(), withScope(store)

AsyncLocalStorage sınıfı kararlı olsa da bazı metotları hâlâ Stability: 1 - Experimental etiketlidir:

  • enterWith(store): Mevcut senkron yürütmenin geri kalanı için bağlama girer ve sonraki asenkron çağrılarda depoyu korur. Belgeler, örneğin bir olay dinleyicisi içinde çağrılırsa sonraki dinleyicilerin de aynı bağlamda çalışacağını gösterir ve güçlü bir neden yoksa run() tercih edilmesini önerir.
  • exit(callback): Callback'i bağlamın dışında, senkron olarak çalıştırır; içerideki getStore() çağrıları undefined döner.
  • disable(): Örneği devre dışı bırakır; sonraki getStore() çağrıları yeniden run() veya enterWith() çağrılana kadar undefined döner. Belgelere göre örneğin çöp toplanabilmesi için çağrılması gerekir.
  • withScope(store): v25.9.0'da eklendi. using bildirimiyle çalışan, blok bitince önceki depoyu geri yükleyen bir RunScope döndürür. Belgeler, async bir fonksiyonda ilk await'ten önce kullanıldığında kapsam değişikliğinin çağıranın bağlamına sızabileceği konusunda uyarır ve asenkron işler için run()'ı önerir.
javascript
import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();
{
  using _ = als.withScope('kapsam');
  console.log('blok içi:', als.getStore());
}
console.log('blok dışı:', als.getStore());
// çıktı: blok içi: kapsam
// çıktı: blok dışı: undefined

Neden async_hooks API'si önerilmiyor?

AsyncLocalStorage ve AsyncResource, node:async_hooks modülünden içe aktarılır, ancak aynı modülün düşük seviyeli kısmı farklı bir durumdadır. Async hooks belgesi Stability: 1 - Experimental etiketlidir ve açıkça bu API'den mümkünse uzaklaşmanızı ister. Belgelere göre createHook, AsyncHook ve executionAsyncResource API'leri kullanılabilirlik sorunları, güvenlik riskleri ve performans etkileri nedeniyle önerilmez; bağlam izleme ihtiyacını kararlı AsyncLocalStorage API'si daha iyi karşılar. Aktif kaynakları izlemek için process.getActiveResourcesInfo(), tanılama verisi için Diagnostics Channel gösterilir. Bunların dışında bir kullanım senaryonuz varsa belgeler Node.js deposunda bir issue açmanızı önerir.

APIKararlılıkÖneri
AsyncLocalStorage (run, getStore)StableBağlam taşımak için kullanın
AsyncResourceStableKendi asenkron kaynaklarınız ve bağlam bağlama için
enterWith, exit, disable, withScopeExperimentalGerekmedikçe run() tercih edin
createHook, AsyncHook, executionAsyncResourceExperimental, önerilmiyorYeni kodda kullanmayın

Sık yapılan hatalar

  • Depoyu run() dışında okumayı beklemek: run() döndükten sonra aynı senkron kodda getStore() undefined döner. Bağlama ihtiyaç duyan kodun run() callback'i içinden başlatılması gerekir.
  • Depoya her istek için paylaşılan bir nesne vermek: Tüm isteklere aynı nesneyi verip içini değiştirmek bağlam ayrımını bozar. Her run() çağrısına yeni bir nesne verin.
  • enterWith() ile beklenmedik sızıntı: Deneysel enterWith() bağlamı mevcut senkron yürütmenin sonuna kadar uygular; bir olay dinleyicisinde kullanmak sonraki dinleyicileri de etkiler.
  • Kendi kuyruğunda callback biriktiren koddan geçerken bağlamı kaybetmek: Böyle yerlerde callback'i AsyncLocalStorage.bind() ya da AsyncResource.bind() ile sarın.
  • Bağlam için doğrudan async_hooks.createHook yazmak: Belgeler bunu önermez; AsyncLocalStorage kullanın.

Sık Sorulan Sorular

AsyncLocalStorage kararlı mı, üretimde kullanılabilir mi?

Evet. YAML kaydına göre sınıf v16.4.0'da Experimental durumundan Stable'a geçti ve v26 belgelerinde Stability: 2 - Stable olarak yer alıyor. Yalnızca enterWith, exit, disable ve withScope metotları deneyseldir.

AsyncLocalStorage ile global değişken arasındaki fark nedir?

Global değişken tüm eşzamanlı istekler arasında paylaşılır; bir istek beklerken diğeri değeri değiştirebilir. AsyncLocalStorage ise her asenkron zincire kendi deposunu bağlar, bu yüzden aynı anda işlenen istekler birbirinin verisini görmez.

Promise ve await bağlamı korur mu?

Evet. run() içinde başlatılan Promise zincirleri, await sonrası kod ve setTimeout, setImmediate gibi zamanlayıcılar depoyu korur; yazıdaki örnekler bunu gösteriyor. Kayıp genellikle geri çağırmaları kendisi biriktirip sonra çalıştıran kodlarda ya da özel thenable uygulamalarında görülür; belgeler bu durumda kodu util.promisify() ile Promise'e çevirmeyi veya AsyncResource kullanmayı önerir.

Birden fazla AsyncLocalStorage örneği kullanabilir miyim?

Evet. Belgelere göre her örnek bağımsız bir bağlam tutar ve birden çok örnek birbirini etkilemeden aynı anda var olabilir. Örneğin biri istek kimliği, diğeri dil tercihi için kullanılabilir.

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: Node.js öğrenme rehberi · node:http ile sunucu · EventEmitter kullanımı · util.promisify ve inspect · Node.js REST API geliştirme