Node.js interview questions: Errors and reliability

Reliability questions are where tidy code-level answers meet messy distributed outcomes. An interviewer may start with `try` and `catch`, unhandled rejections, or graceful shutdown, then ask what happens after the process has already acknowledged work. The important distinction is between detecting an error and preserving an invariant. Senior candidates explain error propagation and resource cleanup. Staff candidates identify duplicate side effects, ambiguous completion, retry ownership, and the durable record that lets a crashed process recover without guessing.

These questions are organized around that shift. The opening answer often recommends logging and retrying, which is not wrong but is incomplete. The follow-up tree asks whether the operation is safe to repeat, what callers observe during shutdown, how a partial write is identified, and when a process should deliberately exit. Answer each prompt as an incident timeline: what changed, what state survived, who retries, and what evidence closes the loop. The unlocked example demonstrates the level of precision expected when "handle errors" is no longer a sufficient design.

Covered in Volume 4: Failure modes and reliability
Question 01Fully unlocked sample

Let's say you own a Node service. How would you set up error handling across the whole thing?

A strong answer treats error handling as routing across Node's separate delivery channels, each with its own crash default, not just try/catch placement

What an AI-prepared candidate might say

So Node gives you a few different ways to handle errors, and I think you kind of want to use all of them together. For synchronous stuff and async/await you use try/catch. Promise chains get a .catch at the end. The older callback APIs are error-first, so you check the first argument before you touch the result. And then streams and EventEmitters emit 'error' events, so you attach a listener for those and the process stays up. On top of that, most teams register handlers for uncaughtException and unhandledRejection as kind of a safety net. In something like Express you'd centralize the rest in an error-handling middleware that logs the error and sends back a clean 500. Generally I try to catch errors close to where they happen and log enough context to debug later. The main thing is one bad request should never take down the whole server.

Senior

The first thing I'd say is Node doesn't have one error channel, it has four, and they don't compose. A try/catch covers synchronous frames plus whatever you await, and that's it. Throw inside a callback and that code is running on a fresh stack rooted in the event loop. Your calling frame returned ages ago, so the surrounding try block is just gone. Then you've got error-first callbacks, promise rejections, and 'error' events, and each of those carries its own crash default. An unobserved rejection kills the process, that's been the default since Node 15. And emit('error') with nobody listening throws synchronously from inside emit itself, which surprises people.

So what I actually do is convert at the boundary. util.promisify or the fs/promises-style APIs for callback code, stream.pipeline and events.once to turn emitter failures into rejections. Once you've done that, almost every failure in the system is an awaited rejection flowing toward a handful of catch points, the per-request wrapper, the job runner, the queue consumer loop. And inside that funnel, each layer wraps with new Error(msg, { cause }) so the classification survives the trip up.

But some things won't convert. Server sockets, connection pools, anything long-lived, there's no single await point for those. So the rule becomes ownership, whoever creates an emitter attaches its 'error' listener in the same breath. And the process-level handlers stay in the design, but as a last line that reports and exits. Never as a place where you keep serving requests.

Staff

Honestly, at this level the design is the easy part. What matters is enforcement and policy, because the whole thing decays without both. The classic outage, and I've lived this one, is a mixed codebase where one raw .pipe chain or one pool socket without a listener throws Unhandled 'error' event at 3am and takes a replica down mid-request. Everything else got converted, that one didn't.

The audit is pretty mechanical. You lint for bare .pipe, for new EventEmitter without an 'error' handler right next to it, and for catch blocks that swallow without rethrowing or classifying. Then measurement. I export counters from process.on('uncaughtExceptionMonitor') and events.errorMonitor so crashes are attributable before the process dies, flush that telemetry synchronously in the exit path, and track crash rate by exit reason instead of one big restart count. Request id goes through AsyncLocalStorage so every error the funnel catches lands in logs with the request that caused it.

And then there's the policy argument, which you have to actually win. Programmer errors crash the process and a supervisor restarts it with backoff. Operational errors get handled at the boundary, retries, shedding, a mapped response. Teams push back because crashing looks worse on the availability dashboards. My defense is state integrity. A process that keeps going past a broken invariant gives you the incidents you cannot reproduce, corrupted cache entries, pool checkouts that never come back, writes applied once and a half. Sure, the funnel costs you some per-callsite nuance and a little stack depth. What it buys is failure behavior you can predict, reproduce in a test, and actually explain during the postmortem.

Follow-up chain

  1. So say I wrap a setTimeout in a try/catch and the callback throws. Why doesn't my catch block fire?
  2. Okay, and if I switch that over to async/await, does the picture change?
  3. Is there ever a case where having a process.on('uncaughtException') handler is actually fine?
  4. What's Error.cause actually buying you over just concatenating the messages?
Question 02First answer included

Say a promise rejects somewhere and nothing ever handles it. What does Node do, and what should your process do about it?

A strong answer nails the exact crash semantics of unhandled rejections and uncaught exceptions, then argues crash versus continue from state integrity

What an AI-prepared candidate might say

So if a promise rejects and nothing handles it, older versions of Node just printed a deprecation warning, but modern Node treats it as fatal and crashes the process. Which is why you always attach a .catch or wrap your await in a try/catch. Most teams I've seen also register process.on('unhandledRejection') and process.on('uncaughtException') handlers so they can log the error and report it to monitoring before anything gets lost. The usual advice is basically log it with the stack trace, send it to your error tracker, and if it looks serious, shut down gracefully, because one bad request shouldn't take down the whole service. And your process manager or orchestrator restarts the app for you anyway, so really the main job is making sure every rejection either gets handled locally or gets caught by a global handler somewhere.

Senior

Pins down exactly when the unhandledRejection event fires, what each --unhandled-rejections mode changes, and why just registering the handler alters crash behavior.

Staff

What you can still safely run in the last hundred milliseconds before exit, and how to keep one poison input from crash-looping an entire fleet.

Follow-up chain

  1. If the log line made it out fine, what's actually dangerous about logging and continuing inside uncaughtException?
  2. So what would you actually let that handler do?
  3. Okay, so if I throw inside an async function, how is that different from an unhandled rejection?
  4. And those --unhandled-rejections flags, what do the different modes actually change?
Question 03First answer included

How do you tell an operational error apart from a programmer error, and why would you treat them differently?

A strong answer classifies errors by what the failure says about process state, and gives each class its own recovery path

What an AI-prepared candidate might say

So operational errors are the failures you kind of expect even when your code is correct. A network timeout, a refused connection, a missing file, a 503 from some downstream service. Your program anticipates those and does something about them, retries, returns an error to the client, falls back to a default. Programmer errors are just bugs. Reading a property of undefined, passing the wrong type, calling an API the wrong way. You can't really handle a bug at runtime, so the standard advice is let the process crash, restart it under a supervisor, and go fix the code. In practice I think teams usually define custom error classes with a flag like isOperational, handle the operational ones close to where they happen, and let everything else propagate up to a top-level handler that logs and exits. The whole point of the distinction is you don't have to wrap every line in a defensive try/catch.

Senior

Why the split is really about invariants, where classification has to happen if it's going to survive layer boundaries, and what `err.code` buys you over `instanceof`.

Staff

How catch-all blocks quietly turn bugs into 'timeouts', the metrics that catch it happening, and the poison-message policy that keeps one bug from crash-looping a whole consumer fleet.

Follow-up chain

  1. So where does something like an out-of-memory error land in this taxonomy?
  2. When you're wrapping errors across layers, how do you keep the classification from getting lost?
  3. And besides the cause, what else would you put in that wrapper?
  4. Say a queue consumer hits a TypeError on one message. Do you crash the worker or just skip the message?
Question 04First answer included

What's special about the 'error' event on an EventEmitter, compared to any other event?

A strong answer knows the 'error' event throws synchronously from inside emit, and treats attaching a listener as taking ownership of the emitter's lifecycle

What an AI-prepared candidate might say

So 'error' is the one event name that EventEmitter treats specially. For any other event, emitting with zero listeners is just a no-op, nothing happens. But if an emitter emits 'error' and nothing is listening, Node throws the error and the process crashes with that Unhandled 'error' event message. Which is why the standard advice is to always attach an 'error' listener to streams, sockets, servers, basically any emitter that can fail. I think the reason these objects report failures as events rather than exceptions or rejections is that the failure happens later, after the original call has already returned, so there's nothing to throw into. Once a listener is attached the error gets delivered like any other event and the process keeps running, and a typical handler just logs the error and cleans up the resource.

Senior

Where the throw really comes from when there's no listener, what errorMonitor can watch that a normal listener can't, and how captureRejections reroutes async listener failures.

Staff

The two ways this fails in production, a crash from the unowned emitter or dead silence from the over-owned one, and the metrics that catch each shape.

Follow-up chain

  1. Say you're awaiting events.once(socket, 'connect') and the socket emits 'error' first. What happens to your await?
  2. And what if nobody's awaiting that promise at all?
  3. What does events.errorMonitor buy you over just attaching a normal 'error' listener?
  4. How do listeners that return promises mix with the 'error' event?
Question 05First answer included

Walk me through what a graceful shutdown actually looks like for a Node HTTP service.

A strong answer sequences the whole shutdown, from the readiness flip at the load balancer through keep-alive teardown, all bounded by a hard deadline

What an AI-prepared candidate might say

So the basic idea is you listen for SIGTERM, stop taking new work, let whatever's in flight finish, release your resources, and exit. Concretely you register a process.on('SIGTERM') handler, call server.close() so the server stops accepting new connections while the existing requests complete, then close your database pools and other clients, and exit with code 0. You also add a timeout so one stuck request can't block shutdown forever, like if the drain hasn't finished in ten seconds or so, you just force-exit anyway. In Kubernetes this lines up with the pod lifecycle. The platform sends SIGTERM, waits out the termination grace period, then sends SIGKILL if the process is still alive. And I'd handle SIGINT the same way so local development stays consistent with production.

Senior

Why server.close() can sit waiting forever on a perfectly healthy server, and the two connection-reaping methods Node added in 18.2 that actually finish the job.

Staff

The full drain ordering, the two race windows hiding inside it, and the deploy-time 5xx forensics that tell you which window you got wrong.

Follow-up chain

  1. So why does server.close() hang forever when there are keep-alive clients around?
  2. And closeIdleConnections versus closeAllConnections, what does each one actually do?
  3. Why bother flipping readiness to failing before you close the listener?
  4. Are there signals you just can't handle, and what does that mean for the design?
Question 06First answer included

A call to a downstream service fails. How do you retry it without making things worse?

A strong answer treats retries as a system property, covering classification, jitter, budgets, and idempotency, not just a loop with a sleep in it

What an AI-prepared candidate might say

Basically you retry with exponential backoff and you cap the attempts. So wait one second, then two, then four, and give up after three to five tries or so. You add jitter, which is just a random component in the delay, so that a bunch of clients that failed at the same moment don't all come back at the same moment. And you only retry errors that are probably transient, like timeouts and 503s, and only on operations that are idempotent, where running them twice is harmless. For writes that aren't naturally idempotent there's the idempotency key thing, which lets the server deduplicate. Honestly libraries handle most of this for you, and a circuit breaker on top stops retrying entirely when the dependency looks properly down. The main things to avoid are retrying forever and hammering a service that's already struggling.

Senior

How to classify what's retryable, full jitter versus plain exponential, per-attempt AbortSignal timeouts, and why a timed-out write is nothing like a refused connection.

Staff

The amplification math when every layer retries, retry budgets, and the idempotency-key mechanics that make retrying a charge actually safe.

Follow-up chain

  1. Okay, a POST times out and the client has no idea whether it committed. What do you do now?
  2. And that idempotency key, where does it live and how long do you keep it around?
  3. Why full jitter and not just plain exponential backoff?
  4. When every layer has its own retries, how do you stop the amplification?
Question 07First answer included

What is a circuit breaker actually doing for a Node service, and when would you bother adding one?

A strong answer explains what the breaker protects inside your own Node process, pending promises, sockets, memory, and pairs it with a bulkhead per dependency

What an AI-prepared candidate might say

So a circuit breaker basically wraps your calls to a dependency and keeps track of failures. In the closed state, calls just pass through normally. When failures cross some threshold, the breaker opens, and after that calls fail immediately without even touching the dependency. That gives the struggling service room to recover, and your users don't sit through the full timeout wait. Then after a cooldown it goes half-open and lets one trial request through. If that succeeds the breaker closes, if it fails it opens again. You'd add one in front of any remote dependency that can fail or hang, so payment providers, third-party APIs, internal services, all of those qualify. Most teams just reach for a library like opossum rather than hand-rolling the state machine. And combined with retries and timeouts, it stops your service from repeatedly hammering something that's already down.

Senior

What actually piles up inside a Node process when a dependency slows down, why you trip on latency too, and the bulkhead that puts a ceiling on the damage per dependency.

Staff

What forty independent breakers do across a fleet, the half-open stampede, and the fallback hierarchy you end up defending from the downstream SLO.

Follow-up chain

  1. Node's single-threaded per process, so what is a bulkhead even bounding there?
  2. And where does http.Agent fit into that picture?
  3. So each of your 40 pods is running its own breaker state. What starts going wrong?
  4. Besides error rate, what else should be able to trip the breaker?
Question 08First answer included

When something fails in the middle of a stream pipeline, how does that error actually travel?

A strong answer can say at the mechanism level what pipe fails to clean up, which stream survives and what leaks, and reaches for pipeline with abort semantics

What an AI-prepared candidate might say

So streams report their failures through 'error' events, and the thing that trips people up is that .pipe() doesn't forward those errors. An error on one stream in the chain never reaches the others, so with a.pipe(b).pipe(c) I'd need an error listener on all three streams, which is easy to forget. Modern code uses stream.pipeline instead. It wires the stages together, forwards any error to a single callback, and cleans up every stream when something fails. There's also a promise-based version in stream/promises that works well with async/await. The rule I go by is pretty simple. .pipe() is fine for quick scripts, but production code should use pipeline, so a failure in any stage tears the whole chain down and hands me one error to handle.

Senior

Exactly what .pipe leaves alive after a destination error, what pipeline destroys and when it does it, and where AbortSignal slots into the teardown.

Staff

How client disconnects quietly leak fds until EMFILE hits, the metrics that spot it early, and why the stalled pipeline that raises no error is the harder incident.

Follow-up chain

  1. Say a client bails halfway through a streamed file download. Without pipeline, what actually leaks?
  2. How would you spot that fd leak before EMFILE takes the whole process down?
  3. When ERR_STREAM_PREMATURE_CLOSE shows up in the logs, what is it actually telling you?
  4. And AbortSignal, where does that fit into a pipeline?
Question 09First answer included

Why do stack traces just vanish across async boundaries, and how do you keep request context attached to your errors?

A strong answer can name the actual mechanism behind async stack loss, and point at the specific places AsyncLocalStorage stops carrying context

What an AI-prepared candidate might say

The stack traces go missing because the callback runs later, on a completely different stack. By the time the async operation completes, the code that started it has already returned, so the trace only shows the completion machinery from the event loop. Async/await mostly fixes this, I believe V8 can stitch the awaited frames together, which is one more reason to prefer it over raw callbacks. Request context is about knowing which request produced an error five layers deep. The standard tool is AsyncLocalStorage. You run each request inside als.run(store, handler), and then anything transitively called can read the store, so your loggers pick up the request id on their own. Combine that with correlation ids passed between services and you get traceable errors without threading a context argument through every function signature.

Senior

How an error's stack gets frozen at construction time, what V8's async reconstruction can and can't stitch back together, and the one propagation rule AsyncLocalStorage follows.

Staff

The connection-pool pattern that hands you someone else's request context, the sampling audit that proves your ids are honest, and where ALS earns its overhead.

Follow-up chain

  1. Why does the trace start at processTicksAndRejections, of all places?
  2. So how does Error.cause get you that missing chain back?
  3. Where exactly does AsyncLocalStorage lose the context?
  4. Between ALS and just passing a parameter explicitly, where do you draw the line?
Question 10First answer included

When you write a health check for a service, what should it actually be checking?

Strong answers split liveness from readiness by blast radius, restart storms versus shed traffic, and design each probe backward from its failure modes

What an AI-prepared candidate might say

So a health check is just an endpoint the platform polls to decide whether your instance is healthy, and the common setup splits it in two. Liveness answers, is the process alive. If it fails, the orchestrator restarts the pod. Readiness answers, can this instance serve traffic right now. If that fails, the instance drops out of the load balancer rotation but keeps running. The liveness one should be a cheap check that the process responds. Readiness can go further and verify that dependencies like the database or the cache are reachable, since serving without them would only produce errors anyway. You keep the endpoints fast, unauthenticated, and off to the side of normal routing. In Kubernetes these map to livenessProbe and readinessProbe, plus there's a startupProbe for apps that boot slowly.

Senior

Which checks belong in liveness versus readiness, how a dependency ping in the wrong probe turns a blip into a fleet restart, and what a blocked event loop does to both.

Staff

How readiness flapping amplifies load into a death spiral, the probe metrics actually worth graphing, and what crash-only design asks of your startup path.

Follow-up chain

  1. So what's actually dangerous about putting a database ping inside a liveness probe?
  2. Okay, so where should that database check live instead?
  3. What happens to your HTTP health probes when the event loop gets blocked?
  4. If you're going crash-only, what does that demand from your startup path?

THE BASELINE GETS YOU THROUGH QUESTION ONE.

Raw Mode trains the follow-ups.

Unlock every Senior and Staff answer, every tree answer, the debug repos, hallucination drills, design scenarios, and the framework capstone.Get NodeBook Raw Mode