Events and Utilities

By Syncfusion®, Inc. · Last updated

The observer pattern and EventEmitter: on, once, off, emit, prependListener, listener helpers, synchronous emit, arguments, the error event, captureRejections, listener limits, extending EventEmitter, events.once and events.on. Error-first callbacks, the util module (promisify, callbackify, inspect, format, types, isDeepStrictEqual, deprecate), cancelling work with AbortController and AbortSignal, and AsyncLocalStorage.

Events and Utilities

Event = a message that says "something happened", such as "the bell rang".

EventEmitter = a Node.js object that sends events and calls the functions that listen for them.

  • Many Node.js objects are event emitters: servers, streams and process.
  • One part of a program emits an event.
  • Other parts listen and react.
  • The two parts do not need to know each other.

Simple lesson: events let one part of a program announce news, and other parts react to it.

A Real-World Example

Think of the school bell.

  • The bell rings once.
  • Class 6 goes to lunch. Class 7 goes to the playground. The office closes the gate.
  • The bell does not call each class one by one.
  • Each class listens for the bell and knows what to do.

The bell is the emitter. Each class is a listener. "Ring" is the event name.

What You Will Learn

In this lesson, you will learn:

  1. The observer pattern and EventEmitter
  2. on, once, off, emit and the listener helpers
  3. Why emit is synchronous, and how to pass data
  4. The special error event and listener limits
  5. Making your own class from EventEmitter
  6. events.once() and events.on() with await
  7. Error-first callbacks and util.promisify
  8. The util toolbox
  9. Cancelling work with AbortController, and AsyncLocalStorage

EventEmitter

Observer pattern = one object keeps a list of listeners and tells all of them when something happens.

  • on(name, fn) = add a listener.
  • once(name, fn) = add a listener that runs only the first time.
  • off(name, fn) = remove a listener. removeListener is the same.
  • emit(name, ...data) = send the event. Returns true if anyone was listening.
  • prependListener(name, fn) = add a listener at the front of the line.

Click ▶ Run and read the terminal.

  • Order: the prepended listener ran first. The rest ran in the order they were added.
  • once: the office listener removed itself after one call, so 3 listeners are left.
  • Data: "lunch" was passed to every listener as an argument.

Listener Helpers

  • listenerCount(name) = how many listeners an event has.
  • listeners(name) = the list of listener functions.
  • eventNames() = all event names that have listeners.
  • removeAllListeners(name) = remove every listener for that name.

Synchronous Emit

emit is synchronous. It calls every listener before the next line runs.

  • Events are not a queue. Nothing waits for the event loop.
  • A slow listener slows down the code that called emit.

EventEmitter is a Node.js tool. Browser code uses addEventListener on page elements instead.

Simple lesson: on adds a listener, emit calls all listeners at once, in order.

Animation: emit sends one event to every listener of that name, in the order they were added bell.emit("ring")the emitter 1. students stand up 2. teacher starts class 3. log "ring"

One emit, many listeners. They run one after another, in the order you added them.

The Special error Event

  • An event named error is special.
  • If you emit error and nobody listens, Node.js throws the error and the program crashes.
  • Always add an error listener to emitters that can fail, such as streams and servers.

captureRejections

  • An async listener returns a promise. If it fails, emit does not notice.
  • new EventEmitter({ captureRejections: true }) sends such failures to the error event.

Listener Limits and Memory Leaks

  • By default, one event may have up to 10 listeners.
  • An 11th listener prints a MaxListenersExceededWarning.
  • This usually means code adds a listener again and again, for example inside a loop. That is a memory leak.
  • If you really need more, call emitter.setMaxListeners(20).

To remove a listener, pass the same function you added. Keep it in a named variable.

Simple lesson: always listen for error, and remove listeners you no longer need.

Extending EventEmitter

Your own classes can send events by extending EventEmitter.

Animation: a Classroom object emits a joined event and three listeners react one after another

Classroom emit("joined", "Asha") Register: mark present Teacher: say hello Counter: count + 1

One emit, three listeners, called one after another.

events.once() and events.on()

  • events.once(emitter, name) = a promise that waits for one event. Use it with await.
  • events.on(emitter, name) = an async iterator. Use it with for await to handle many events.

Simple lesson: extend EventEmitter to give your own objects events, and await once() to wait for one.

Callback Conventions

Error-first callback = a callback whose first argument is an error, or null when all went well.

fs.readFile("marks.txt", "utf8", (err, data) => {
  if (err) {
    // handle the problem
    return;
  }
  // use data
});
  • Older Node.js APIs use callbacks.
  • Newer APIs return promises, such as node:fs/promises.
  • Most callback APIs have a promise twin. Prefer the promise version in new code.

fs/promises, timers/promises, stream/promises, readline/promises and dns/promises are the promise versions of their modules.

Simple lesson: in a callback API, always check err first.

The util Module

node:util is a toolbox of small helpers.

  • promisify(fn) = turn an error-first callback function into a function that returns a promise.
  • callbackify(fn) = the opposite: turn an async function into a callback function.
  • inspect(value) = the text console.log would print for a value.
  • format("%s has %d", a, b) = fill in a text template.
  • types = checks such as util.types.isPromise(x).
  • isDeepStrictEqual(a, b) = are two objects equal, inside and out?
  • deprecate(fn, message) = wrap an old function so it prints a warning once.

Simple lesson: reach for util before writing your own helper, especially promisify.

Cancelling Operations

AbortController = a remote control that can cancel work.

  • const ac = new AbortController() makes the controller.
  • ac.signal = the signal you pass to the work.
  • ac.abort() = press cancel. The work stops with an AbortError.
  • AbortSignal.timeout(ms) = a signal that cancels by itself after ms milliseconds.
  • Many Node.js APIs take { signal }: timers/promises, events.once, fs/promises, child_process, http and fetch.

Simple lesson: pass a signal to slow work, so you can cancel it.

AsyncLocalStorage (Advanced)

AsyncLocalStorage = a store that follows one chain of async work, such as one web request.

  • A server handles many requests at the same time.
  • You want every log line to show which request it belongs to.
  • Passing a request id through every function is tiring.
  • als.run(store, fn) starts a chain. als.getStore() reads the store anywhere inside it, even after await.
  • Common uses: request ids in logs, the current user, timing.
// main.mjs
import { AsyncLocalStorage } from "node:async_hooks";
import { setTimeout as wait } from "node:timers/promises";

const als = new AsyncLocalStorage();

function log(msg) {
  console.log(`[request ${als.getStore().id}] ${msg}`);
}

async function handle(id, delay) {
  await als.run({ id }, async () => {
    log("started");
    await wait(delay);
    log("finished");
  });
}

await Promise.all([handle(1, 30), handle(2, 10)]);

Output:

[request 1] started
[request 2] started
[request 2] finished
[request 1] finished
  • Request 2 finished first, but each line still shows the right id.

Simple lesson: AsyncLocalStorage remembers "which request am I in" across await.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
No error listenerAn unhandled error event is ignoredAlways add on("error", ...)The program crashes
Adding a listener inside a loop or a requestListeners clean themselves upAdd once, or remove with offMemory leak and MaxListenersExceededWarning
Removing a listener with a new arrow functionoff matches by codeKeep a name for the function and pass the same oneThe listener is never removed
Expecting emit to wait for the event loopEvents are queuedemit calls listeners at onceCode runs in a surprising order
Ignoring err in a callbackerr is only for loggingCheck err first and returnCode uses undefined data
Using promisify on a function without an error-first callbackIt works on any callbackOnly for (err, value) callbacksThe promise never settles, or the order is wrong

TL;DR

  1. EventEmitter = sends named events to listeners.

    • on add, once one time, off remove, emit send
  2. emit is synchronous = listeners run before the next line.
  3. error event = crashes the program if nobody listens.
  4. Listener limit = 10 by default. More usually means a leak.
  5. events.once = await one event.
  6. Error-first callback = (err, value). Check err first.
  7. util.promisify = callback function to promise function.
  8. AbortController = cancel work through a signal.

Check Yourself

Exercises

What to Remember

  • An EventEmitter calls its listeners at once, in order, when you emit.
  • once listens one time. off needs the same function you added.
  • Always handle the error event.
  • Extend EventEmitter to give your own classes events.
  • util.promisify turns old callbacks into promises.
  • Pass an AbortSignal to slow work so it can be cancelled.

Next: Error Handling. You will read error messages, handle failures and decide when a program should stop.