Node.js'te EventEmitter, adlandırılmış olaylar yayan (emit) ve bu olaylara bağlanan fonksiyonları (listener, dinleyici) çağıran sınıftır. node:events modülünden gelir; on() ile dinleyici eklersiniz, emit() ile olayı tetiklersiniz, once() ile yalnızca bir kez çalışacak dinleyici kaydedersiniz. En önemli kural şudur: emit() çağrıldığında dinleyiciler senkron olarak, kayıt sırasıyla çalışır ve 'error' olayının hiç dinleyicisi yoksa hata fırlatılır, süreç kapanır.

Node.js çekirdeğinin büyük bölümü bu yapının üzerine kuruludur: bir net.Server her bağlantıda, bir dosya okuma akışı dosya açıldığında, bir stream veri geldiğinde olay yayar. Bu yazıda EventEmitter'ın temel metotlarını, 'error' olayının özel davranışını, dinleyici sınırı uyarısını, async dinleyicilerdeki tuzağı ve events.once() / events.on() ile Promise tabanlı kullanımı Node.js v26.13.0 üzerinde çalıştırılmış örneklerle anlatıyoruz. Serinin tamamı için Node.js öğrenme rehberi sayfasına bakabilirsiniz.

EventEmitter nedir, nereden gelir?

EventEmitter bir sınıftır ve iki biçimde kullanılır: doğrudan new EventEmitter() ile bir nesne oluşturursunuz ya da kendi sınıfınızı class Kuyruk extends EventEmitter şeklinde ondan türetirsiniz. Belgelere göre olay yayan bütün Node.js nesneleri bu sınıfın örnekleridir. Olay adları genellikle camelCase yazılmış string'lerdir; ancak geçerli herhangi bir özellik anahtarı, örneğin bir Symbol, de olay adı olabilir.

ESM'de import { EventEmitter } from 'node:events', CommonJS'te const EventEmitter = require('node:events') yazarsınız. CommonJS'te modülün kendisi sınıftır; ESM'de hem varsayılan dışa aktarım hem de adlandırılmış EventEmitter kullanılabilir. İki modül sisteminin farkları için CommonJS ve ESM karşılaştırmasına göz atabilirsiniz. Modül, Node.js belgelerinde Stability: 2 - Stable olarak işaretlidir.

on() ve emit(): dinleyici eklemek ve olay yaymak

emitter.on(olayAdi, dinleyici) dinleyiciyi o olayın listesinin sonuna ekler ve emitter'ın kendisini döndürür, bu yüzden çağrılar zincirlenebilir. emitter.emit(olayAdi, ...arguman) ise o olaya kayıtlı tüm dinleyicileri sırayla çağırır, verdiğiniz argümanları her birine iletir ve olayın en az bir dinleyicisi varsa true, yoksa false döndürür. Dinleyicilerin döndürdüğü değerler yok sayılır.

javascript
import { EventEmitter } from 'node:events';

const siparis = new EventEmitter();

siparis.on('olusturuldu', (id, tutar) => {
  console.log(`Sipariş ${id} oluşturuldu, tutar: ${tutar} TL`);
});
siparis.on('olusturuldu', function (id) {
  console.log('ikinci dinleyici, this === siparis:', this === siparis);
});

console.log('emit öncesi');
const vardi = siparis.emit('olusturuldu', 1001, 250);
console.log('emit sonrası, dinleyici var mıydı:', vardi);
console.log('olmayan olay:', siparis.emit('iptal'));
// çıktı: emit öncesi
// çıktı: Sipariş 1001 oluşturuldu, tutar: 250 TL
// çıktı: ikinci dinleyici, this === siparis: true
// çıktı: emit sonrası, dinleyici var mıydı: true
// çıktı: olmayan olay: false

Çıktının sırası önemli: emit sonrası satırı iki dinleyici de bittikten sonra yazıldı. emit() bir kuyruğa iş bırakıp hemen dönmez; dinleyicileri o anda, aynı çağrı yığınında çalıştırır. Belgeler bunun olayların doğru sırayla işlenmesini sağladığını ve yarış durumlarını önlediğini belirtir. Bir dinleyicinin işini sonraya bırakması gerekiyorsa içinde setImmediate() veya process.nextTick() kullanabilir; bu iki fonksiyonun farkını process.nextTick ve setImmediate yazısında ayrıntılı anlatıyoruz.

İkinci dinleyici normal function olarak yazıldığı için içindeki this, dinleyicinin bağlı olduğu emitter'ı gösterir. Ok fonksiyonu (arrow function) kullanırsanız bu bağlama yapılmaz; this çevreleyen kapsamdan gelir. ESM modülünün en üst düzeyinde bu değer undefined'dır. this kurallarının genel anlatımı için this anahtar kelimesi yazısına bakın.

once(), prependListener() ve dinleyici listesini okumak

once(), prependListener() ve dinleyici listesini okumak: on / addListener, once, prependListener, off / removeListener

once() ile eklenen dinleyici, olay ilk kez yayıldığında önce listeden çıkarılır, sonra çağrılır; ikinci emit() onu artık görmez. Varsayılan olarak dinleyiciler eklenme sırasıyla çalışır. Bir dinleyiciyi listenin başına koymak isterseniz prependListener(), tek seferlik bir dinleyiciyi başa koymak isterseniz prependOnceListener() kullanırsınız.

javascript
import { EventEmitter } from 'node:events';

const ee = new EventEmitter();
let n = 0;
ee.once('baglandi', () => console.log('once çalıştı', ++n));
ee.on('veri', () => console.log('a'));
ee.prependListener('veri', () => console.log('b (başa eklendi)'));

ee.emit('baglandi');
ee.emit('baglandi');
ee.emit('veri');
console.log(ee.listenerCount('baglandi'), ee.listenerCount('veri'));
console.log(ee.eventNames());
// çıktı: once çalıştı 1
// çıktı: b (başa eklendi)
// çıktı: a
// çıktı: 0 2
// çıktı: [ 'veri' ]

listenerCount(olay) kayıtlı dinleyici sayısını, eventNames() dinleyicisi olan olay adlarını, listeners(olay) ise dinleyici dizisinin bir kopyasını verir. rawListeners() da kopya döndürür ama once() tarafından oluşturulan sarmalayıcı fonksiyonları da içerir. Bu metotlar özellikle hata ayıklarken ve testlerde, bir dinleyicinin gerçekten kaldırılıp kaldırılmadığını görmek için işe yarar.

MetotNe yaparDönüş
on / addListenerDinleyiciyi listenin sonuna ekleremitter
onceBir kez çalışacak dinleyici ekleremitter
prependListenerDinleyiciyi listenin başına ekleremitter
off / removeListenerDinleyicinin en fazla bir kopyasını çıkarıremitter
removeAllListenersTüm dinleyicileri ya da bir olayınkileri sileremitter
emitDinleyicileri senkron çağırırtrue veya false
listenerCountDinleyici sayısı (isteğe bağlı belirli fonksiyon için)sayı
eventNamesDinleyicisi olan olay adlarıdizi

Dinleyici kaldırmak: off() ve removeListener()

Dinleyici kaldırmak: off() ve removeListener()

off(), removeListener()'ın takma adıdır. Kaldırmak için dinleyiciye referansınız olmalıdır; yani dinleyiciyi isimli bir değişkende tutmanız gerekir. Satır içinde yazılmış anonim bir ok fonksiyonunu sonradan kaldıramazsınız. Belgelerde belirtilen iki ayrıntı sık karıştırılır. Birincisi, on() aynı fonksiyonun daha önce eklenip eklenmediğini kontrol etmez; aynı fonksiyonu iki kez eklerseniz iki kez çağrılır ve removeListener() her çağrıda yalnızca bir kopyayı çıkarır. İkincisi, bir emit() başladığında o anki dinleyici listesi sabitlenir; bir dinleyicinin o sırada başka bir dinleyiciyi kaldırması, devam eden yayını etkilemez.

javascript
import { EventEmitter } from 'node:events';

const ee = new EventEmitter();
const kaydet = () => console.log('kaydet');
const bildir = () => {
  console.log('bildir');
  ee.off('kayit', kaydet2);
};
const kaydet2 = () => console.log('kaydet2');

ee.on('kayit', kaydet);
ee.on('kayit', bildir);
ee.on('kayit', kaydet2);

ee.emit('kayit');
console.log('---');
ee.emit('kayit');
console.log('---');
ee.on('kayit', kaydet);
console.log('kaydet kaç kez kayıtlı:', ee.listenerCount('kayit', kaydet));
ee.removeListener('kayit', kaydet);
console.log('bir off sonrası:', ee.listenerCount('kayit', kaydet));
// çıktı: kaydet
// çıktı: bildir
// çıktı: kaydet2
// çıktı: ---
// çıktı: kaydet
// çıktı: bildir
// çıktı: ---
// çıktı: kaydet kaç kez kayıtlı: 2
// çıktı: bir off sonrası: 1

İlk yayında bildir, kaydet2'yi kaldırdığı hâlde kaydet2 yine çalıştı; çünkü liste yayın başında kopyalanmıştı. İkinci yayında artık listede değil. listenerCount()'a ikinci argüman olarak fonksiyon verdiğinizde, o fonksiyonun listede kaç kez bulunduğunu sayar. removeAllListeners() için belgelerdeki uyarı açıktır: başka bir modülün oluşturduğu emitter'lardan (soketler, dosya akışları gibi) sizin eklemediğiniz dinleyicileri silmek kötü bir uygulamadır.

'error' olayı: dinleyici yoksa süreç kapanır

'error', EventEmitter için özel bir olay adıdır. Bir emitter'da hata olduğunda beklenen davranış 'error' olayını yaymaktır. Bu olay için kayıtlı hiç dinleyici yoksa Node.js hatayı fırlatır, yığın izini (stack trace) yazar ve süreç sonlanır. Aşağıdaki betik bu yüzden son satıra hiç ulaşmaz:

javascript
import { EventEmitter } from 'node:events';

const ee = new EventEmitter();
ee.emit('error', new Error('bağlantı koptu'));
console.log('bu satır hiç çalışmaz');
// çıktı (stderr, yollar kısaltıldı, çıkış kodu 1):
// file:///home/kullanici/proje/ev3.mjs:4
// ee.emit('error', new Error('bağlantı koptu'));
//                  ^
//
// Error: bağlantı koptu
//     at file:///home/kullanici/proje/ev3.mjs:4:18
//     ...
// Emitted 'error' event at:
//     at file:///home/kullanici/proje/ev3.mjs:4:4
//     ...
//
// Node.js v26.13.0

Belgelerin önerdiği en iyi uygulama, hata yayabilecek her emitter'a bir 'error' dinleyicisi eklemektir. Yalnızca hataları kaydetmek, yani tüketmeden izlemek istiyorsanız events.errorMonitor sembolüyle dinleyici ekleyebilirsiniz. Bu dinleyiciler normal 'error' dinleyicilerinden önce çağrılır, ancak davranışı değiştirmez: başka bir 'error' dinleyicisi yoksa süreç yine kapanır.

javascript
import { EventEmitter, errorMonitor } from 'node:events';

const ee = new EventEmitter();
ee.on(errorMonitor, (err) => console.log('izleme:', err.message));
ee.on('error', (err) => console.log('işlendi:', err.message));

ee.emit('error', new Error('disk dolu'));
console.log('süreç çalışmaya devam ediyor');
// çıktı: izleme: disk dolu
// çıktı: işlendi: disk dolu
// çıktı: süreç çalışmaya devam ediyor

Async dinleyiciler ve captureRejections

Dinleyiciye async fonksiyon vermek mümkündür, ama emit() dönen Promise'i beklemez ve onunla ilgilenmez. Async dinleyici içinde fırlatılan hata reddedilmiş bir Promise'e dönüşür ve bu Promise'i kimse yakalamadığı için işlenmemiş ret (unhandled rejection) oluşur. Belgeler bu durumu açıkça sorunlu olarak niteler. Async fonksiyonların genel davranışı için async/await yazısına bakabilirsiniz.

Çözüm captureRejections seçeneğidir. Yapıcıya { captureRejections: true } verdiğinizde Node.js dinleyicinin döndürdüğü Promise'e bir ret işleyicisi bağlar ve hatayı asenkron olarak emitter'ın Symbol.for('nodejs.rejection') metoduna, bu metot yoksa 'error' olayına yönlendirir. EventEmitter.captureRejections = true ise bundan sonra oluşturulan tüm emitter'ların varsayılanını değiştirir.

javascript
import { EventEmitter } from 'node:events';

const ee = new EventEmitter({ captureRejections: true });
ee.on('kaydet', async () => {
  throw new Error('veritabanı yanıt vermedi');
});
ee.on('error', (err) => console.log('error olayına geldi:', err.message));

ee.emit('kaydet');
console.log('emit döndü');
// çıktı: emit döndü
// çıktı: error olayına geldi: veritabanı yanıt vermedi

Çıktıda emit döndü önce geliyor; hata 'error' olayına asenkron olarak iletiliyor. Belgelerdeki bir uyarıyı da not edin: captureRejections ile üretilen 'error' olaylarına, sonsuz hata döngüsünü önlemek için ayrıca bir yakalama işleyicisi eklenmez. Bu yüzden 'error' dinleyicisinin kendisini async yazmamanız önerilir.

Dinleyici sınırı ve MaxListenersExceededWarning

Varsayılan olarak bir olaya 10'dan fazla dinleyici eklendiğinde Node.js stderr'e bir uyarı yazar. Bu katı bir sınır değildir; dinleyici yine eklenir. Amaç, örneğin her istekte aynı emitter'a yeni dinleyici ekleyip hiç kaldırmamak gibi bellek sızıntılarını erken fark ettirmektir. Uyarı process.on('warning') ile yakalanabilir; adı MaxListenersExceededWarning'dir ve emitter, type, count alanlarını taşır.

javascript
import { EventEmitter } from 'node:events';

process.on('warning', (w) => {
  console.log(w.name, '| olay:', w.type, '| sayı:', w.count);
});

const ee = new EventEmitter();
console.log('varsayılan sınır:', ee.getMaxListeners());
for (let i = 0; i < 11; i++) {
  ee.on('istek', () => {});
}
console.log('eklenen dinleyici:', ee.listenerCount('istek'));
// çıktı: varsayılan sınır: 10
// çıktı: eklenen dinleyici: 11
// çıktı: MaxListenersExceededWarning | olay: istek | sayı: 11
// stderr: (node:96604) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 istek listeners added to [EventEmitter]. MaxListeners is 10. Use emitter.setMaxListeners() to increase limit
// stderr: (Use `node --trace-warnings ...` to show where the warning was created)

Uyarı gerçekten çok sayıda dinleyiciye ihtiyaç duyduğunuz meşru bir durumdan geliyorsa emitter.setMaxListeners(n) ile o nesnenin sınırını yükseltebilirsiniz; Infinity veya 0 sınırsız anlamına gelir. events.defaultMaxListeners değiştirildiğinde bu, değişiklikten önce oluşturulanlar dahil tüm emitter'ları etkiler, bu yüzden dikkatli kullanılmalıdır; nesne bazında setMaxListeners() her zaman önceliklidir. Uyarının kaynağını bulmak için --trace-warnings bayrağıyla çalıştırmak yığın izini gösterir. Sınırı yükseltmeden önce sızıntı olup olmadığını kontrol edin; bellek sızıntısı analizi için Node.js performans optimizasyonu yazısına bakabilirsiniz.

events.once() ve events.on(): Promise ve async iterator ile olay beklemek

node:events modülü, emitter'ları Promise dünyasına bağlayan iki yardımcı fonksiyon sunar. events.once(emitter, ad), olay yayıldığında çözülen bir Promise döndürür; değeri, olaya verilen tüm argümanlardan oluşan bir dizidir. Beklerken emitter 'error' yayarsa Promise reddedilir. events.on(emitter, ad) ise olayları sırayla veren bir async iterator döndürür; for await...of ile dolaşılır, 'error' gelirse hata fırlatır ve döngüden çıkıldığında eklediği dinleyicileri temizler.

javascript
import { EventEmitter, once, on } from 'node:events';

const ee = new EventEmitter();

setTimeout(() => ee.emit('hazir', 'v1', 42), 10);
const degerler = await once(ee, 'hazir');
console.log('once:', degerler);

setTimeout(() => {
  ee.emit('satir', 'birinci');
  ee.emit('satir', 'ikinci');
  ee.emit('bitti');
}, 10);

for await (const [satir] of on(ee, 'satir', { close: ['bitti'] })) {
  console.log('satır:', satir);
}
console.log('döngü bitti, kalan dinleyici:', ee.listenerCount('satir'));
// çıktı: once: [ 'v1', 42 ]
// çıktı: satır: birinci
// çıktı: satır: ikinci
// çıktı: döngü bitti, kalan dinleyici: 0

close seçeneği yinelemeyi bitirecek olay adlarını alır; bu örnekte 'bitti' gelince döngü kapanır. close vermezseniz döngü siz break yapana ya da bir AbortSignal iptal edene kadar bekler. İki fonksiyon da signal seçeneğini kabul eder; sinyal iptal edilirse bekleme AbortError ile sonlanır. Bu konuyu AbortController yazısında ayrıca işliyoruz. Async iterator ve for await sözdiziminin kendisi için generator ve iterator yazısına göz atın.

javascript
import { EventEmitter, once } from 'node:events';

const ee = new EventEmitter();
const ac = new AbortController();

setTimeout(() => ac.abort(), 20);
try {
  await once(ee, 'asla', { signal: ac.signal });
} catch (err) {
  console.log(err.name, err.code);
}

setTimeout(() => ee.emit('error', new Error('patladı')), 5);
try {
  await once(ee, 'veri');
} catch (err) {
  console.log('once reddedildi:', err.message);
}
// çıktı: AbortError ABORT_ERR
// çıktı: once reddedildi: patladı

Mini örnek: EventEmitter'dan türeyen bir iş kuyruğu

Gerçek projelerde EventEmitter genellikle bir sınıfın temel sınıfı olarak kullanılır. Aşağıdaki Kuyruk sınıfı her eklemede 'eklendi', üç işe ulaşınca 'doldu' olayını yayar. ekle() metodu this döndürdüğü için çağrılar zincirlenebiliyor; 'doldu' dinleyicisi once() ile eklendiği için dördüncü eklemede tekrar çalışmıyor. Sınıf sözdizimi ve özel alanlar (#isler) için JavaScript sınıflar yazısına bakın.

javascript
import { EventEmitter } from 'node:events';

class Kuyruk extends EventEmitter {
  #isler = [];

  ekle(is) {
    this.#isler.push(is);
    this.emit('eklendi', is, this.#isler.length);
    if (this.#isler.length >= 3) this.emit('doldu');
    return this;
  }
}

const k = new Kuyruk();
k.on('eklendi', (is, uzunluk) => console.log(`+ ${is} (toplam ${uzunluk})`));
k.once('doldu', () => console.log('kuyruk doldu, işleme başla'));

k.ekle('fatura').ekle('e-posta').ekle('rapor').ekle('yedek');
console.log(k instanceof EventEmitter);
// çıktı: + fatura (toplam 1)
// çıktı: + e-posta (toplam 2)
// çıktı: + rapor (toplam 3)
// çıktı: kuyruk doldu, işleme başla
// çıktı: + yedek (toplam 4)
// çıktı: true

Bu sınıfta olaylar ekle() çağrıldığında yayıldığı için, dinleyiciler nesne oluşturulduktan sonra eklenebiliyor. Olayı yapıcının (constructor) içinden yayarsanız henüz kimse dinlemiyor olur ve olay kaybolur. Bu durumun çözümü olan process.nextTick() desenini nextTick ve setImmediate yazısında çalışan bir örnekle gösteriyoruz.

EventEmitter ile EventTarget farkı

EventEmitter ile EventTarget farkı: Dinleyici ekleme, Aynı fonksiyonu iki kez eklemek, error olayı, Dinleyici hata fırlatırsa

Node.js, web standardı olan EventTarget ve Event sınıflarını da global olarak sunar; AbortSignal gibi bazı API'ler bu modeli kullanır. Belgelere göre Node.js'teki EventTarget'ta tarayıcıdaki gibi bir hiyerarşi ve olay yayılımı (propagation) yoktur. En önemli davranış farkları şöyle özetlenebilir:

ÖzellikEventEmitterEventTarget
Dinleyici eklemeon, once, prependListeneraddEventListener (once seçeneğiyle)
Aynı fonksiyonu iki kez eklemekİki kez eklenir ve çağrılırNodeEventTarget için yok sayılır
error olayıDinleyici yoksa fırlatılır, süreç kapanırÖzel davranış yok
Dinleyici hata fırlatırsaHata emit çağrısına yayılır, sonraki dinleyiciler çalışmazDiğer dinleyiciler yine çalışır, hata yakalanmamış istisna olur
Async dinleyici reddicaptureRejections ile yakalanırOtomatik yakalanır
Dinleyici sayısını görmeklistenerCount metoduevents.listenerCount veya getEventListeners

Sık yapılan hatalar

  • 'error' dinleyicisi eklememek: Dinleyicisiz bir 'error' olayı süreci kapatır. Kendi emitter'larınızda ve stream, soket gibi çekirdek nesnelerde mutlaka 'error' dinleyin.
  • emit()'in asenkron olduğunu sanmak: Dinleyiciler senkron çalışır; uzun süren bir dinleyici emit()'i çağıran kodu da bekletir. Ağır işleri dinleyicinin içinde sonraya bırakın.
  • Anonim dinleyiciyi kaldırmaya çalışmak: off() fonksiyon referansı ister. Satır içinde yazdığınız ok fonksiyonunun referansı yoktur.
  • Her istekte dinleyici ekleyip kaldırmamak: Uzun yaşayan bir emitter'a her HTTP isteğinde on() eklemek, MaxListenersExceededWarning ile kendini gösteren bir sızıntıdır. once() kullanın ya da işi bitince off() çağırın.
  • Async dinleyicideki hatayı unutmak: captureRejections olmadan async dinleyicinin fırlattığı hata işlenmemiş ret olur. Seçeneği açın ya da dinleyicinin içinde try/catch kullanın.
  • Uyarıyı susturmak için sınırı Infinity yapmak: setMaxListeners(Infinity) uyarıyı kaldırır ama sızıntıyı gidermez. Önce dinleyicilerin neden biriktiğini bulun.

Sık Sorulan Sorular

EventEmitter senkron mu çalışır?

Evet. emit() çağrıldığında o olaya kayıtlı bütün dinleyiciler kayıt sırasıyla ve senkron olarak çağrılır; emit() ancak hepsi bitince döner. Asenkronluk ancak dinleyicinin içinde setImmediate(), bir zamanlayıcı ya da asenkron bir I/O işlemi başlatmasıyla gelir.

on() ile addListener() arasında fark var mı?

Hayır. Belgelere göre addListener(), on()'un takma adıdır. Benzer şekilde off() da removeListener()'ın takma adıdır. Hangisini kullandığınız yalnızca okunabilirlik tercihidir.

'error' olayını dinlemezsem ne olur?

Emitter 'error' yaydığında hiç dinleyici yoksa hata nesnesi fırlatılır, yığın izi stderr'e yazılır ve süreç sonlanır. errorMonitor ile eklenen izleme dinleyicileri bunu engellemez; süreci korumak için normal bir 'error' dinleyicisi gerekir.

MaxListenersExceededWarning uyarısını nasıl düzeltirim?

Önce gerçekten 10'dan fazla dinleyiciye ihtiyacınız olup olmadığına bakın; çoğu zaman uyarı, kaldırılmayan dinleyicilerden kaynaklanır. Meşru bir ihtiyaçsa ilgili nesnede setMaxListeners(n) ile sınırı yükseltin. Kaynağı bulmak için uygulamayı --trace-warnings ile çalıştırın; CLI bayrakları için NODE_OPTIONS ve CLI bayrakları yazısına bakabilirsiniz.

Bir olayı await ile bekleyebilir miyim?

Evet, node:events modülündeki once() fonksiyonu tam bunun içindir: const [deger] = await once(emitter, 'olay'). Olay akışını sırayla işlemek için for await (const args of on(emitter, 'olay')) kullanabilirsiniz.

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 · process.nextTick ve setImmediate farkı · AbortController ile iptal · Node.js stream nedir · uncaughtException ve unhandledRejection · Node.js nedir