NodeJSOperational vs Programmer Errors

Operational vs Programmer Errors

Not all errors are the same kind of problem. A user submitting a bad email address is a normal, expected event; your code calling undefined.property is a bug. Treating them identically — either by crashing the process on every user mistake, or by silently continuing after a programming bug — are both wrong. The classic Node.js distinction, articulated by Joyent's error-handling guide and still the standard mental model, is operational errors vs programmer errors.

The distinction

Operational error

Programmer error

Definition

Expected failure of a correct program

A bug — the code is wrong

Examples

File not found, DB down, invalid input, 404

undefined.x, wrong arg type, assertion failure

Cause

Environment, user input, external service

Developer mistake

Response

Handle gracefully, return HTTP error

Log, then crash or alert for a fix

Should crash?

No

Yes (or restart cleanly)

Operational errors are expected and handled; programmer errors signal broken code
An operational error is something your program anticipated when written correctly — "what if the database is down?", "what if this file doesn't exist?" These are part of the contract; your code handles them and responds appropriately. A programmer error is different: it means your code has an incorrect assumption (`null` where an object was expected, a missing required argument). The right response is not to "handle" it and continue — a program in an unknown state is dangerous — but to **log it fully and let the process exit**, then fix the bug.
Operational errors — handle and respond

JS
// These are operational — expected, handled, user gets a meaningful response:

// 1. User input invalid:
if (!email.includes('@')) throw new HttpError(400, 'Invalid email')

// 2. Resource not found:
const user = await db.users.findById(id)
if (!user) throw new HttpError(404, 'User not found')

// 3. External service down:
try {
  await paymentService.charge(amount)
} catch (err) {
  if (err.code === 'ECONNREFUSED') throw new HttpError(503, 'Payment service unavailable')
  throw err
}

// 4. Database unique constraint:
try {
  await db.users.create({ email })
} catch (err) {
  if (err.code === '23505') throw new HttpError(409, 'Email already registered')
  throw err
}
Programmer errors — log and crash

JS
// These are bugs — they should NEVER happen in correct code:

// TypeError: can't read property of undefined
const name = user.profile.name  // programmer forgot to check user exists

// Wrong argument type — should be a number, got undefined
function divide(a, b) {
  if (typeof a !== 'number' || typeof b !== 'number') {
    throw new TypeError(`divide expects numbers, got ${typeof a} and ${typeof b}`)
  }
}

// Failed assertion
import assert from 'node:assert'
assert(config.dbUrl, 'DATABASE_URL must be set')   // programmer error if missing at startup
Don't try to recover from programmer errors — they signal unknown state
If `user` is `null` when your code assumed it would never be, the program is in a state its author didn't reason about. Silently swallowing the `TypeError` and continuing means everything that follows operates on wrong assumptions — potentially corrupting data, skipping security checks, or serving wrong responses. The safer choice is to let the process crash (or call an alert and restart via a supervisor like PM2 or Kubernetes). The process comes back up in a known-good state; the bug is logged for a developer to fix.
How to tell them apart
  • Ask: "could a correct, deployed program encounter this error at runtime?" If yes → operational.

  • TypeError, ReferenceError, failed assert → almost always programmer errors.

  • Network errors, file-not-found, DB constraint violations → operational.

  • If you have to read the stack trace to understand why it happened → probably a programmer error.

  • If a user action directly triggered it → probably operational.

In the centralised error handler

JS
app.use((err, req, res, next) => {
  const statusCode = err.statusCode ?? 500
  const isOperational = statusCode < 500

  // Log everything, but with different severity:
  if (isOperational) {
    logger.warn({ err, path: req.path }, 'Operational error')
  } else {
    logger.error({ err, path: req.path }, 'Programmer/unknown error')
    // Optionally: notify alerting (Sentry, PagerDuty, etc.)
  }

  // Never expose internals to the client:
  res.status(statusCode).json({
    error: isOperational ? err.message : 'Internal server error',
    code:  isOperational ? err.code   : 'INTERNAL_ERROR',
  })
})
Errors without a statusCode are assumed to be programmer errors — default 500
A missing `statusCode` on an error means it bubbled up without being translated — a library error, an unexpected `TypeError`, or something genuinely unforeseen. Treat these as programmer errors: log the full detail (stack trace, request context), send a generic 500, and investigate. Only errors your code deliberately throws with an `HttpError` or explicit `statusCode` are treated as operational.
Next
Build a clean domain vocabulary with typed errors: [Custom Error Classes](/nodejs/custom-errors).