callbacks
multiplexing
asynchronous programming
event handling
software development

Multiplexing callbacks

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Callback multiplexing means taking one event source and safely dispatching it to many listeners. It is useful in UI event systems, background task completion hooks, and internal service event buses. The challenge is preserving predictable behavior when listeners fail, unsubscribe, or run asynchronously.

What Multiplexing Should Guarantee

A good callback multiplexer defines clear rules:

  • Registration and unregistration are explicit.
  • Listener execution order is deterministic or documented.
  • One failing listener does not break all others.
  • One-time listeners are supported for lifecycle events.

Without these rules, callback systems become fragile and difficult to debug.

Build a Minimal Multiplexer

Start with a small event bus abstraction.

javascript
1class CallbackBus {
2  constructor() {
3    this.listeners = new Map();
4  }
5
6  on(topic, fn) {
7    const arr = this.listeners.get(topic) || [];
8    arr.push(fn);
9    this.listeners.set(topic, arr);
10    return () => this.off(topic, fn);
11  }
12
13  off(topic, fn) {
14    const arr = this.listeners.get(topic) || [];
15    this.listeners.set(topic, arr.filter((x) => x !== fn));
16  }
17
18  emit(topic, payload) {
19    const arr = this.listeners.get(topic) || [];
20    for (const fn of arr) {
21      fn(payload);
22    }
23  }
24}
25
26const bus = new CallbackBus();
27const unsubA = bus.on("done", (x) => console.log("A", x));
28bus.on("done", (x) => console.log("B", x));
29
30bus.emit("done", { id: 1 });
31unsubA();
32bus.emit("done", { id: 2 });

This baseline covers fan-out and unsubscribe mechanics.

Add Failure Isolation

One listener throwing an exception should not prevent others from running.

javascript
1emit(topic, payload) {
2  const arr = this.listeners.get(topic) || [];
3  const errors = [];
4
5  for (const fn of arr) {
6    try {
7      fn(payload);
8    } catch (err) {
9      errors.push(err);
10    }
11  }
12
13  if (errors.length > 0) {
14    console.error("callback errors", errors);
15  }
16}

If failure should abort dispatch, make that a documented policy instead of accidental behavior.

Support One-Time Listeners

Lifecycle events often need listeners that run once and auto-remove.

javascript
1once(topic, fn) {
2  const wrapper = (payload) => {
3    this.off(topic, wrapper);
4    fn(payload);
5  };
6  return this.on(topic, wrapper);
7}

This avoids manual unsubscribe boilerplate and reduces leak risk.

Async Listener Handling

If listeners can return promises, decide whether dispatch should wait for all listeners or fire-and-forget.

Await-all pattern:

javascript
1async emitAsync(topic, payload) {
2  const arr = this.listeners.get(topic) || [];
3  const tasks = arr.map(async (fn) => {
4    try {
5      await fn(payload);
6      return null;
7    } catch (err) {
8      return err;
9    }
10  });
11
12  const results = await Promise.all(tasks);
13  const errors = results.filter(Boolean);
14  if (errors.length) {
15    console.error("async listener errors", errors);
16  }
17}

This model is useful when downstream state should not progress until all listeners finish.

Prevent Listener Leaks

Multiplexers are leak-prone when listeners are never removed. Use unsubscribe handles and lifecycle hooks to clean up.

Guidelines:

  • Return unsubscribe function from on.
  • Use once for transient listeners.
  • Clear listeners on component teardown.
  • Track listener counts for diagnostics in long-lived processes.

A small leak in callback systems can cause duplicate side effects and memory growth over time.

Testing Multiplexing Behavior

Important tests:

  • Multiple listeners receive same payload.
  • Unsubscribe prevents future callbacks.
  • One-time listener runs once.
  • One listener failure does not block others.
  • Async mode resolves all listeners before completion.

These tests lock behavior and make refactoring safe.

Common Pitfalls

  • Not returning unsubscribe handles from registration.
  • Letting one callback exception abort the full fan-out path unintentionally.
  • Mixing sync and async listeners without a defined dispatch policy.
  • Mutating listener arrays while iterating without defensive handling.
  • Forgetting cleanup in long-lived modules and creating callback leaks.

Summary

  • Callback multiplexing needs explicit delivery, error, and lifecycle policies.
  • Start with a small bus that supports on, off, and emit.
  • Add failure isolation so one bad listener does not break all listeners.
  • Support once and async dispatch modes based on use case.
  • Treat unsubscribe discipline as essential for correctness and memory safety.

Course illustration
Course illustration

All Rights Reserved.