Events #
Event-driven programming adalah paradigma inti di Node.js — hampir semua operasi asinkron di Node.js dibangun di atas sistem event. HTTP server memancarkan event saat request masuk, stream memancarkan event saat data tersedia, proses memancarkan event saat sinyal diterima. Di balik semua ini ada satu kelas: EventEmitter. Memahami EventEmitter secara mendalam membuka pintu untuk membangun komponen yang loosely coupled, observable, dan mudah dikembangkan — karena kamu bisa menambah perilaku baru tanpa mengubah kode yang sudah ada, cukup dengan menambah listener baru.
EventEmitter Dasar #
import { EventEmitter } from "events";
// buat instance EventEmitter
const emitter = new EventEmitter();
// daftarkan listener — fungsi yang dipanggil saat event terjadi
emitter.on("pesan", (teks: string) => {
console.log("Pesan diterima:", teks);
});
// pancarkan event dengan data
emitter.emit("pesan", "Halo dari EventEmitter!");
// Output: Pesan diterima: Halo dari EventEmitter!
// satu event bisa punya banyak listener
emitter.on("pesan", (teks: string) => {
console.log("Listener kedua:", teks.toUpperCase());
});
emitter.emit("pesan", "test");
// Output:
// Pesan diterima: test
// Listener kedua: TEST
// emit mengembalikan true jika ada listener, false jika tidak ada
const adaListener = emitter.emit("event-tanpa-listener");
console.log(adaListener); // false
Typed EventEmitter #
Masalah terbesar dengan EventEmitter bawaan adalah tidak ada type safety — kamu bisa emit event dengan nama apapun dan payload apapun tanpa error TypeScript. Ada beberapa pola untuk mengatasinya.
Pola dengan Generic Class #
import { EventEmitter } from "events";
// definisikan map antara nama event dan tipe payload-nya
interface EventMap {
[event: string | symbol]: unknown[];
}
// typed EventEmitter yang enforce nama event dan tipe payload
class TypedEmitter<T extends EventMap> {
private emitter = new EventEmitter();
on<K extends keyof T & (string | symbol)>(
event: K,
listener: (...args: T[K] extends unknown[] ? T[K] : never) => void
): this {
this.emitter.on(event as string, listener as (...args: unknown[]) => void);
return this;
}
once<K extends keyof T & (string | symbol)>(
event: K,
listener: (...args: T[K] extends unknown[] ? T[K] : never) => void
): this {
this.emitter.once(event as string, listener as (...args: unknown[]) => void);
return this;
}
off<K extends keyof T & (string | symbol)>(
event: K,
listener: (...args: T[K] extends unknown[] ? T[K] : never) => void
): this {
this.emitter.off(event as string, listener as (...args: unknown[]) => void);
return this;
}
emit<K extends keyof T & (string | symbol)>(
event: K,
...args: T[K] extends unknown[] ? T[K] : never
): boolean {
return this.emitter.emit(event as string, ...args);
}
removeAllListeners(event?: keyof T & (string | symbol)): this {
this.emitter.removeAllListeners(event as string | undefined);
return this;
}
}
// definisikan events untuk domain tertentu
interface PesananEvents extends EventMap {
"pesanan:dibuat": [pesanan: Pesanan];
"pesanan:dibayar": [pesananId: string, jumlah: number];
"pesanan:dikirim": [pesananId: string, noResi: string];
"pesanan:selesai": [pesananId: string];
"pesanan:dibatalkan": [pesananId: string, alasan: string];
}
interface Pesanan {
id: string;
userId: string;
items: Array<{ produkId: string; jumlah: number; harga: number }>;
total: number;
}
// penggunaan — TypeScript akan error jika nama event atau payload salah
const pesananEmitter = new TypedEmitter<PesananEvents>();
pesananEmitter.on("pesanan:dibuat", (pesanan) => {
// pesanan di-infer sebagai Pesanan — autocomplete bekerja
console.log(`Pesanan baru: ${pesanan.id}, total: Rp ${pesanan.total}`);
});
pesananEmitter.on("pesanan:dibayar", (pesananId, jumlah) => {
// pesananId: string, jumlah: number — tipe sudah benar
console.log(`Pembayaran ${pesananId}: Rp ${jumlah}`);
});
// TypeScript error — "pesanan:tidak-ada" bukan event yang valid
// pesananEmitter.on("pesanan:tidak-ada", () => {}); // ✗ compile error
Pola dengan Subclass EventEmitter #
Untuk komponen yang lebih besar, extend EventEmitter secara langsung:
import { EventEmitter } from "events";
interface KeranjangEvents {
"item:ditambah": [produkId: string, jumlah: number];
"item:dihapus": [produkId: string];
"keranjang:dikosongkan": [];
"total:berubah": [total: number];
}
// deklarasi untuk menggabungkan tipe event ke class
declare interface Keranjang {
on<K extends keyof KeranjangEvents>(
event: K,
listener: (...args: KeranjangEvents[K]) => void
): this;
emit<K extends keyof KeranjangEvents>(
event: K,
...args: KeranjangEvents[K]
): boolean;
}
class Keranjang extends EventEmitter {
private items = new Map<string, { jumlah: number; harga: number }>();
tambahItem(produkId: string, jumlah: number, harga: number): void {
const existing = this.items.get(produkId);
if (existing) {
existing.jumlah += jumlah;
} else {
this.items.set(produkId, { jumlah, harga });
}
this.emit("item:ditambah", produkId, jumlah);
this.emit("total:berubah", this.hitungTotal());
}
hapusItem(produkId: string): void {
if (this.items.delete(produkId)) {
this.emit("item:dihapus", produkId);
this.emit("total:berubah", this.hitungTotal());
}
}
kosongkan(): void {
this.items.clear();
this.emit("keranjang:dikosongkan");
this.emit("total:berubah", 0);
}
private hitungTotal(): number {
let total = 0;
for (const { jumlah, harga } of this.items.values()) {
total += jumlah * harga;
}
return total;
}
get jumlahItem(): number {
return this.items.size;
}
}
// penggunaan
const keranjang = new Keranjang();
keranjang.on("item:ditambah", (produkId, jumlah) => {
console.log(`+ ${jumlah}x ${produkId}`);
});
keranjang.on("total:berubah", (total) => {
console.log(`Total: Rp ${total.toLocaleString("id-ID")}`);
});
keranjang.tambahItem("laptop-asus", 1, 15_000_000);
keranjang.tambahItem("mouse-logitech", 2, 350_000);
// Output:
// + 1x laptop-asus
// Total: Rp 15.000.000
// + 2x mouse-logitech
// Total: Rp 15.700.000
Metode EventEmitter #
on, once, off #
const emitter = new EventEmitter();
// on — listener permanen, dipanggil setiap kali event dipancarkan
const handler = (data: string) => console.log("on:", data);
emitter.on("data", handler);
// once — listener sekali pakai, otomatis dihapus setelah dipanggil pertama kali
emitter.once("koneksi", () => {
console.log("Terhubung! (hanya sekali)");
});
emitter.emit("koneksi"); // "Terhubung! (hanya sekali)"
emitter.emit("koneksi"); // tidak ada output — listener sudah dihapus
// off / removeListener — hapus listener tertentu
// PENTING: harus referensi fungsi yang SAMA dengan yang didaftarkan
emitter.off("data", handler);
emitter.emit("data", "test"); // tidak ada output — handler sudah dihapus
// ANTI-PATTERN: tidak bisa hapus arrow function inline
emitter.on("event", (x: number) => console.log(x)); // ✗ tidak bisa dihapus nanti
// BENAR: simpan referensi ke fungsi
const myHandler = (x: number) => console.log(x);
emitter.on("event", myHandler);
emitter.off("event", myHandler); // ✓ bisa dihapus
// removeAllListeners — hapus semua listener untuk satu event atau semua event
emitter.removeAllListeners("data"); // hapus semua listener "data"
emitter.removeAllListeners(); // hapus SEMUA listener dari semua event
Listener Prepend #
// addListener — sama dengan on
emitter.addListener("event", handler);
// prependListener — tambah listener di AWAL antrian (dipanggil pertama)
emitter.on("proses", () => console.log("Listener 1"));
emitter.on("proses", () => console.log("Listener 2"));
emitter.prependListener("proses", () => console.log("Listener 0 (prepend)"));
emitter.emit("proses");
// Output:
// Listener 0 (prepend)
// Listener 1
// Listener 2
// prependOnceListener — prepend + sekali pakai
emitter.prependOnceListener("proses", () => console.log("Sekali di awal"));
Inspeksi Listener #
const emitter = new EventEmitter();
const h1 = () => {};
const h2 = () => {};
emitter.on("event", h1);
emitter.on("event", h2);
emitter.on("lain", h1);
// eventNames — daftar event yang punya listener
console.log(emitter.eventNames()); // ["event", "lain"]
// listenerCount — jumlah listener untuk event tertentu
console.log(emitter.listenerCount("event")); // 2
console.log(emitter.listenerCount("lain")); // 1
// listeners — array fungsi listener untuk event tertentu
const listeners = emitter.listeners("event");
console.log(listeners.length); // 2
console.log(listeners[0] === h1); // true
// rawListeners — seperti listeners, tapi wrapper once juga dikembalikan
const raw = emitter.rawListeners("event");
Batas Jumlah Listener #
// default maksimal 10 listener per event
// jika melebihi, Node.js menampilkan warning di stderr
const emitter = new EventEmitter();
// ubah batas untuk instance tertentu
emitter.setMaxListeners(20);
// ubah batas global untuk semua EventEmitter baru
EventEmitter.defaultMaxListeners = 20;
// set ke 0 untuk unlimited (hati-hati memory leak!)
emitter.setMaxListeners(0);
// cek batas saat ini
console.log(emitter.getMaxListeners()); // 20
// ANTI-PATTERN: tambah listener dalam loop tanpa cleanup
// ini cepat memenuhi batas dan menyebabkan memory leak
function setupHandlerSalah(emitter: EventEmitter, count: number): void {
for (let i = 0; i < count; i++) {
emitter.on("event", () => console.log(i)); // ✗ listener menumpuk
}
}
// BENAR: satu listener yang menangani logika di dalam
function setupHandlerBenar(emitter: EventEmitter, items: number[]): void {
emitter.on("event", (data: unknown) => {
for (const item of items) {
console.log(item, data); // ✓ satu listener, banyak item
}
});
}
Error Event — Penanganan Error di EventEmitter #
Event error memiliki perilaku khusus di Node.js: jika dipancarkan tanpa listener, proses akan crash dengan uncaught exception.
const emitter = new EventEmitter();
// ANTI-PATTERN: emit error tanpa listener
emitter.emit("error", new Error("Sesuatu yang buruk terjadi"));
// ✗ proses crash: Error: Sesuatu yang buruk terjadi
// BENAR: selalu daftarkan listener untuk event "error"
emitter.on("error", (err: Error) => {
console.error("EventEmitter error:", err.message);
// tangani error — jangan sampai proses crash
});
emitter.emit("error", new Error("Sesuatu yang buruk terjadi"));
// ✓ Output: EventEmitter error: Sesuatu yang buruk terjadi
// pattern yang baik: class yang extend EventEmitter selalu daftarkan error handler default
class KoneksiDB extends EventEmitter {
private connected = false;
constructor() {
super();
// error handler default — bisa di-override oleh consumer
this.on("error", (err: Error) => {
console.error("[KoneksiDB] Unhandled error:", err.message);
});
}
connect(url: string): void {
try {
// simulasi koneksi
if (!url.startsWith("postgresql://")) {
throw new Error(`URL tidak valid: ${url}`);
}
this.connected = true;
this.emit("connect");
} catch (err) {
this.emit("error", err instanceof Error ? err : new Error(String(err)));
}
}
query(sql: string): void {
if (!this.connected) {
this.emit("error", new Error("Belum terhubung ke database"));
return;
}
// eksekusi query...
this.emit("data", { sql, rows: [] });
}
}
Event Sekali Pakai dan Promise #
Menggunakan once() bersama Promise sangat berguna untuk menunggu event tertentu sebelum melanjutkan eksekusi.
import { EventEmitter, once } from "events";
// events.once() — helper untuk await satu event
async function tungguEvent(
emitter: EventEmitter,
namaEvent: string,
timeoutMs?: number
): Promise<unknown[]> {
if (timeoutMs !== undefined) {
// dengan timeout menggunakan AbortController
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const args = await once(emitter, namaEvent, { signal: controller.signal });
clearTimeout(timer);
return args;
} catch (err: any) {
clearTimeout(timer);
if (err.name === "AbortError") {
throw new Error(`Timeout menunggu event "${namaEvent}"`);
}
throw err;
}
}
return once(emitter, namaEvent);
}
// contoh: tunggu server siap sebelum melanjutkan
class Server extends EventEmitter {
async start(port: number): Promise<void> {
setTimeout(() => {
this.emit("ready", port);
}, 100); // simulasi startup
}
}
const server = new Server();
server.start(3000);
const [port] = await tungguEvent(server, "ready", 5000);
console.log(`Server siap di port ${port}`);
// pola async iterator untuk stream event
import { on } from "events";
async function prosesEventStream(emitter: EventEmitter): Promise<void> {
// on() dari 'events' membuat async iterator dari event
for await (const [data] of on(emitter, "data")) {
console.log("Data diterima:", data);
// break untuk menghentikan iterasi
if (data === null) break;
}
}
Pattern Observer dengan EventEmitter #
EventEmitter adalah implementasi natural dari pola Observer — objek (observable) memiliki daftar dependents (observer) yang dinotifikasi saat state berubah.
flowchart LR
A[Observable\nEventEmitter] -- emit event --> B[Observer 1\nListener]
A -- emit event --> C[Observer 2\nListener]
A -- emit event --> D[Observer 3\nListener]
E[Kode lain] -- emit\ntrigger --> A// contoh: state management sederhana berbasis EventEmitter
interface AppState {
user: { id: string; nama: string } | null;
tema: "light" | "dark";
bahasa: string;
}
type StateEvents = {
[K in keyof AppState as `state:${K}:changed`]: [
newValue: AppState[K],
oldValue: AppState[K]
];
} & {
"state:changed": [key: keyof AppState, newValue: unknown];
};
class Store extends EventEmitter {
private state: AppState = {
user: null,
tema: "light",
bahasa: "id",
};
getState(): Readonly<AppState> {
return { ...this.state };
}
setState<K extends keyof AppState>(key: K, value: AppState[K]): void {
const oldValue = this.state[key];
if (JSON.stringify(oldValue) === JSON.stringify(value)) {
return; // tidak ada perubahan — tidak perlu emit
}
this.state[key] = value;
// emit event spesifik untuk key yang berubah
this.emit(`state:${key}:changed` as string, value, oldValue);
// emit event umum untuk semua perubahan
this.emit("state:changed", key, value);
}
}
// penggunaan
const store = new Store();
// observer spesifik untuk perubahan user
store.on("state:user:changed", (newUser, oldUser) => {
if (newUser) {
console.log(`Login: ${newUser.nama}`);
} else {
console.log(`Logout dari: ${oldUser?.nama}`);
}
});
// observer untuk semua perubahan state
store.on("state:changed", (key, value) => {
console.log(`State berubah: ${String(key)} =`, value);
});
store.setState("user", { id: "u1", nama: "Budi" });
// Output:
// Login: Budi
// State berubah: user = { id: 'u1', nama: 'Budi' }
store.setState("tema", "dark");
// Output:
// State berubah: tema = dark
Menghindari Memory Leak #
Memory leak dengan EventEmitter terjadi saat listener ditambahkan tapi tidak pernah dihapus — terutama dalam komponen yang dibuat ulang berkali-kali.
// ANTI-PATTERN: listener tidak dibersihkan saat komponen dihancurkan
class KomponenSalah {
private emitter: EventEmitter;
constructor(emitter: EventEmitter) {
this.emitter = emitter;
// ✗ listener ini tidak pernah dihapus meski KomponenSalah di-garbage collect
this.emitter.on("data", this.handleData.bind(this));
}
private handleData(data: unknown): void {
console.log(data);
}
// tidak ada cleanup!
}
// BENAR: selalu sediakan metode destroy/cleanup
class Komponen {
private boundHandler: (data: unknown) => void;
constructor(private emitter: EventEmitter) {
// simpan referensi ke bound handler agar bisa dihapus nanti
this.boundHandler = this.handleData.bind(this);
this.emitter.on("data", this.boundHandler);
}
private handleData(data: unknown): void {
console.log(data);
}
// panggil ini saat komponen tidak lagi dibutuhkan
destroy(): void {
this.emitter.off("data", this.boundHandler);
}
}
// pola dengan AbortController untuk cleanup otomatis
function listenSekali(
emitter: EventEmitter,
event: string,
handler: (...args: unknown[]) => void,
signal: AbortSignal
): void {
if (signal.aborted) return;
emitter.on(event, handler);
// hapus listener saat signal di-abort
signal.addEventListener("abort", () => {
emitter.off(event, handler);
}, { once: true });
}
// penggunaan dengan AbortController
const controller = new AbortController();
const emitter = new EventEmitter();
listenSekali(emitter, "data", (d) => console.log(d), controller.signal);
emitter.emit("data", "pesan 1"); // diproses
emitter.emit("data", "pesan 2"); // diproses
controller.abort(); // cleanup otomatis
emitter.emit("data", "pesan 3"); // tidak diproses — listener sudah dihapus
EventEmitter vs Pattern Lain #
Gunakan EventEmitter untuk:
✓ Komponen yang perlu diobservasi oleh banyak pihak tanpa coupling langsung
✓ Domain event — sesuatu yang "terjadi" dalam sistem (pesanan dibuat, user login)
✓ Progress tracking — laporkan kemajuan operasi panjang ke observer
✓ Plugin system — izinkan kode eksternal bereaksi terhadap event internal
✓ Integrasi dengan Node.js stream, HTTP server, dan API berbasis event lainnya
Pertimbangkan alternatif jika:
✗ Hanya satu listener — callback atau Promise lebih sederhana
✗ Butuh type safety ketat tanpa boilerplate — pertimbangkan library seperti mitt atau eventemitter3
✗ State management kompleks — pertimbangkan pattern seperti Redux atau Zustand
✗ Komunikasi antar service — gunakan message broker (Redis pub/sub, RabbitMQ, Kafka)
Ringkasan #
- Selalu daftarkan listener
errorpada setiap EventEmitter yang bisa memancarkan error — jika tidak ada listener saatemit("error", ...)dipanggil, proses Node.js akan crash.- Simpan referensi listener untuk bisa dihapus — arrow function inline tidak bisa di-
off()karena setiap kali ditulis menghasilkan fungsi yang berbeda; selalu simpan ke variabel.once()dari moduleventsmemungkinkanawaitsatu event secara langsung — jauh lebih bersih dari membungkus dengannew Promise()manual.- Typed EventEmitter dengan interface adalah investasi yang worth it untuk project besar — mencegah typo nama event dan payload yang salah tipe yang baru terdeteksi saat runtime.
- Pantau jumlah listener dengan
listenerCount()dan beri batas wajar dengansetMaxListeners()— warning “possible memory leak” dari Node.js adalah sinyal nyata, bukan sekadar peringatan.- Selalu sediakan metode
destroy()atau cleanup di komponen yang mendaftarkan listener ke emitter eksternal — listener yang tidak dibersihkan adalah sumber memory leak yang paling umum dengan EventEmitter.prependListener()untuk handler yang harus berjalan sebelum handler lain — berguna untuk middleware atau interceptor yang perlu memproses event sebelum handler utama.- Gunakan
on()dari moduleventsuntuk mengiterasi event sebagai async iterator — sangat bersih untuk memproses stream event denganfor await...of.