Skip to the solution
MediumLow-Level Design7 min read

What are 'Event Emitting' patterns and how does error-first callback convention relate to them?

1.2k views

The problem

Question presented to candidate: "An operation can either take an error-first callback, or be modeled as an EventEmitter firing 'data'/'error' events. If the caller forgets to handle a failure in each style, does the same thing happen both times?"

What a strong answer should cover:

  • Error-first callback convention β€” fn(...args, (err, result) => {}) β€” and EventEmitter-based patterns β€” .emit("data", result) / .emit("error", err) β€” are Node's two standard idioms for delivering an async result: one single callback invocation for a one-shot operation, versus multiple, ongoing events for something that can fire more than once (a stream's repeated 'data' events, a socket's connection lifecycle).
  • πŸ“Œ The critical, verified behavioral difference between the two, when a failure is not handled: a callback invoked with an Error that the caller's own callback body simply ignores produces no crash at all β€” verified directly, the error is silently swallowed, and the script continues normally. Emitting 'error' on an EventEmitter with no listener attached, by contrast, throws synchronously β€” verified directly as a real, caught exception β€” and, left uncaught further up, crashes the process.
  • This is not a minor implementation detail β€” it is a deliberate design choice: error-first callbacks put the burden of checking err entirely on the caller, with no enforcement; EventEmitter's 'error' special case (covered fully, with its own live verification, in the dedicated EventEmitter-basics question) makes ignoring a failure impossible to do silently β€” it becomes a loud crash instead.
  • The choice between the two patterns in your own API design should track the shape of the result: a single, one-time outcome (reading a file once) fits an error-first callback (or, in modern code, a Promise) naturally; a stream of ongoing events (a socket receiving many messages over its lifetime, a long-running watcher) fits EventEmitter naturally.
  • A precise answer connects this to Node's broader evolution: Promises/async-await (covered in their own dedicated questions) have largely superseded error-first callbacks for single-result async operations in modern code, while EventEmitter remains the standard, unreplaced idiom for ongoing, multi-event sources precisely because Promises only ever resolve/reject once.
  • The takeaway worth stating explicitly: silently ignorable errors are a real, structural risk of the error-first callback pattern specifically β€” a caller who writes fn(() => {}), discarding the err parameter entirely, introduces no visible symptom until the swallowed failure causes a much harder-to-trace problem downstream.

Clarifying questions expected:

  • "Is the operation genuinely one-shot, or does it produce results/events repeatedly over time?" β€” the deciding factor for which pattern actually fits.
  • "Is the concern designing a new API, or understanding why an existing one behaves the way it does on a missed error?"

Code / implementation expected: Yes β€” the actual, verified contrast between a silently-ignored callback error (no crash) and an unhandled 'error' emit (a real synchronous throw) is the concrete, convincing deliverable.

  • async
  • patterns
  • events

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read Β· 9 sections

Read the code

JavaScriptThe same 'unhandled error' situation in both patterns β€” a callback silently swallows it, EventEmitter throws synchronously
const EventEmitter = require("events");

// Error-first callback: the caller simply ignores the err parameter
function op(cb) { cb(new Error("boom")); }
op(() => {}); // NO crash, no warning at all
console.log("callback style: error silently ignored, script continues");

// EventEmitter: emit("error") with NO listener attached
const e = new EventEmitter();
try {
  e.emit("error", new Error("boom"));
} catch (err) {
  console.log("EventEmitter style: emit(error) with no listener THROWS:", err.message);
}

// Output:
// callback style: error silently ignored, no crash, script continues
// EventEmitter style: emit(error) with no listener THROWS: boom

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 42 of 152 in Node.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Node.js questions
Up nextMedium

When would you choose Node.js over other backend technologies?

Continue