Chapter 3 — Error Codes Below Your App
ECONNRESET, Exit Code 137, SQLSTATE 23505 — and What Each One Physically Means
Part 1: Foundations. Prerequisites: Chapters 1–2.
Opening
Your JavaScript runs on top of a stack of other systems: the operating system, the network, TLS, the container or process manager, the database. When one of them fails, it reports the failure with a code — short, cryptic, and extremely precise.
ECONNREFUSED and ETIMEDOUT both mean "could not connect", but they describe completely different physical situations and send you to completely different places. Exit code 137 and exit code 134 both mean "the process died", but one means the container ran out of memory and the other means the JavaScript heap did. SQLSTATE 40001 means "retry this", while 23505 means "never retry this."
This chapter decodes the codes you will see most. The goal is a reflex: see the code, know the layer, know where to look — before opening a single file.
Part 1: Where These Codes Come From
When Node asks the operating system to do something — open a file, connect a socket, listen on a port — it makes a system call (syscall). If the syscall fails, the OS returns an error number called an errno, which has a symbolic name like ECONNREFUSED. Node puts that name on the error as err.code.
Read a Node system error as a sentence:
Error: connect ECONNREFUSED 127.0.0.1:5432
│ │ └── the address and port it tried
│ └── the code: what went wrong
└── the syscall: what Node was trying to do
The error object also carries these as fields: err.syscall ('connect'), err.code ('ECONNREFUSED'), err.address and err.port. "Connect refused to port 5432" already tells you it is Postgres and that the problem is before any query ran.
The E prefix means "error". The rest is an abbreviation: CONNREFUSED = connection refused, NOENT = no entry (no such file), ACCES = access denied, ADDRINUSE = address in use.
Part 2: Network Codes — Mapped to the Life of a Connection
A network request goes through stages. Each code belongs to a stage, and knowing the stage tells you where to look.
Stage 1 — DNS lookup fails
| Code | What physically happened | Where to look |
|---|---|---|
ENOTFOUND (getaddrinfo ENOTFOUND api.internal) | DNS has no record for that name | Typo in the hostname, wrong env var, or an internal name that only resolves inside a particular network (a Docker Compose service name used from your laptop, a VPC-only hostname) |
EAI_AGAIN | DNS lookup failed temporarily — the resolver did not answer | The DNS server is down, overloaded or unreachable; often transient |
Stage 2 — TCP connect fails
| Code | What physically happened | Where to look |
|---|---|---|
ECONNREFUSED | The machine answered, but nothing is listening on that port, so it refused immediately | The service is not running, wrong port, or it listens only on 127.0.0.1. Inside a container, localhost means the container itself, not your machine or another container |
ETIMEDOUT | No answer at all — packets went out and nothing came back before the timeout | A firewall or security group silently dropping traffic, a wrong IP, a host that is down, or a server too overloaded to accept connections |
EHOSTUNREACH / ENETUNREACH | There is no network route to that host or network | Network config, VPN not connected, wrong subnet |
The difference between refused and timed out is the most useful distinction in network debugging. Refused is fast and means "reached the machine, nobody home." Timed out is slow and means "never reached anything that answered." A refused connection points at the service; a timeout points at the network path.
Stage 3 — the connection existed, then broke
| Code | What physically happened | Where to look |
|---|---|---|
ECONNRESET | The other side abruptly closed an open connection (sent a TCP reset) | The server crashed or restarted mid-request, a proxy or load balancer killed the connection, or a keep-alive race (below) |
socket hang up (code ECONNRESET) | Node's HTTP client: the server closed the connection without sending any response | Server crash, server-side timeout, or the same keep-alive race |
EPIPE | You wrote to a connection the other side had already closed | Writing a response after the client disconnected, or streaming to a dead socket |
Node's fetch wraps all of these
Node's built-in fetch (powered by a library called undici) throws a generic TypeError: fetch failed and puts the real error in cause:
TypeError: fetch failed
[cause]: Error: connect ECONNREFUSED 127.0.0.1:3000
It also has its own codes, prefixed UND_ERR_: UND_ERR_CONNECT_TIMEOUT (connect took too long), UND_ERR_HEADERS_TIMEOUT (connected, but the server took too long to start responding), UND_ERR_SOCKET (the socket failed). If your error handling only logs err.message, every one of these looks identical: "fetch failed". Always log err.cause.
The keep-alive race — where mysterious ECONNRESETs come from
HTTP clients keep connections open after a request (keep-alive) so the next request can skip the TCP and TLS handshakes. Servers close idle connections after a timeout — Node's HTTP server defaults to 5 seconds. If the client picks an idle connection from its pool at the exact moment the server closes it, the request dies.
The same race between a load balancer and your app produces intermittent 502 errors, which Chapter 13 investigates in full.
Part 3: Filesystem, Port and Process Codes
| Code | Typical message | What it means |
|---|---|---|
ENOENT | ENOENT: no such file or directory, open '/app/config.json' | The path does not exist — often a relative path resolved from a different working directory than you assumed |
ENOENT (on spawn) | spawn git ENOENT | The program you tried to run is not installed or not on PATH in that environment (very common in minimal containers) |
EACCES | EACCES: permission denied, open '/var/log/app.log' | The process's user lacks permission. Also listen EACCES when a non-root process binds a port below 1024 |
EPERM | operation not permitted | The OS forbids the operation regardless of file permissions; on Windows often a file locked by another process |
EADDRINUSE | listen EADDRINUSE: address already in use :::3000 | Another process is already listening on that port — often an old instance of your own server. Find it with lsof -i :3000 |
EMFILE | EMFILE: too many open files | The process hit its limit of open file descriptors. Usually a leak — sockets or files opened and never closed — and sometimes a genuinely low limit |
ENOSPC | ENOSPC: no space left on device | The disk is full. On Linux dev machines, also System limit for number of file watchers reached — the file-watcher limit, not disk |
EEXIST | EEXIST: file already exists | Creating something that is already there |
EISDIR / ENOTDIR | illegal operation on a directory | You treated a directory as a file, or the other way round |
Node's own ERR_ codes
Node also has codes for its own errors, prefixed ERR_. The ones you will meet most:
| Code | Message | What it means |
|---|---|---|
ERR_HTTP_HEADERS_SENT | Cannot set headers after they are sent to the client | Your handler sent a response twice — usually a missing return after res.send() in an error branch, or both a callback and a promise responding |
MODULE_NOT_FOUND / ERR_MODULE_NOT_FOUND | Cannot find module 'x' | The package is not installed in this environment, the path is wrong, or a file extension is missing in ES-module imports |
ERR_REQUIRE_ESM | require() of ES Module … not supported | CommonJS code tried to require an ES-module-only package |
ERR_INVALID_ARG_TYPE | The "path" argument must be of type string. Received undefined | You passed undefined to a Node API — usually a missing env var or config value |
ERR_INVALID_URL | Invalid URL | new URL() got a relative or malformed string — often a missing base URL env var |
Part 4: TLS and Certificate Errors
HTTPS adds a TLS handshake after TCP connect. During it, the client checks the server's certificate: is it signed by a trusted authority, is it still valid, and does it match the hostname? Each failure has its own code.
| Node code | Browser equivalent | What it means |
|---|---|---|
CERT_HAS_EXPIRED | NET::ERR_CERT_DATE_INVALID | The certificate's validity period has ended (or the client's clock is wrong) |
ERR_TLS_CERT_ALTNAME_INVALID | NET::ERR_CERT_COMMON_NAME_INVALID | The certificate is valid but for a different hostname — you are calling the wrong name, or the cert does not list this one |
DEPTH_ZERO_SELF_SIGNED_CERT / SELF_SIGNED_CERT_IN_CHAIN | NET::ERR_CERT_AUTHORITY_INVALID | Signed by an authority the client does not trust — a self-signed dev cert, or a corporate proxy that intercepts HTTPS with its own certificate |
UNABLE_TO_VERIFY_LEAF_SIGNATURE / UNABLE_TO_GET_ISSUER_CERT_LOCALLY | (often works in browsers) | The server did not send its intermediate certificate. Browsers often fill the gap themselves; Node does not — so "works in the browser, fails from Node" |
Never "fix" these with NODE_TLS_REJECT_UNAUTHORIZED=0. It turns off certificate checking for the whole process, which means you will happily talk to an attacker. Fix the certificate, or give Node the right CA with NODE_EXTRA_CA_CERTS.
Part 5: Chrome's net::ERR_ Codes
In the browser, network failures show up in the Console and Network tab as net::ERR_…. They map almost one-to-one onto the Node codes above.
| Chrome code | Same as | What it means |
|---|---|---|
ERR_NAME_NOT_RESOLVED | ENOTFOUND | DNS lookup failed |
ERR_CONNECTION_REFUSED | ECONNREFUSED | Nothing listening on that port |
ERR_CONNECTION_TIMED_OUT | ETIMEDOUT | No answer |
ERR_CONNECTION_RESET | ECONNRESET | Connection abruptly closed |
ERR_EMPTY_RESPONSE | socket hang up | Server closed the connection without responding |
ERR_INTERNET_DISCONNECTED | — | The device is offline |
ERR_CERT_… | TLS codes above | Certificate problem |
ERR_TOO_MANY_REDIRECTS | — | A redirect loop, often between http and https or between two auth pages |
ERR_BLOCKED_BY_CLIENT | — | A browser extension blocked it — usually an ad blocker matching a URL like /analytics or /track. Not your bug, but your users have it |
ERR_ABORTED | AbortError | The request was cancelled — navigation, an AbortController, or a superseded request. Often harmless |
ERR_FAILED | — | A generic failure; for cross-origin requests, check the Console for a CORS message |
ERR_HTTP2_PROTOCOL_ERROR | — | The HTTP/2 stream was broken, often by a proxy or a server error mid-response |
Part 6: Process Exit Codes and Signals
When a process ends, it reports an exit code: 0 means success, anything else means failure. When a process is killed by a signal, the exit code is 128 + the signal number. This is how you tell "the app crashed" from "something killed the app."
| Exit code | Meaning | What usually happened |
|---|---|---|
0 | Success | The process finished normally |
1 | General failure | In Node: an uncaught exception or unhandled rejection, or process.exit(1) |
126 | Found but not executable | Missing execute permission on a script |
127 | Command not found | The binary is not installed or not on PATH |
130 | Killed by SIGINT (2) | Someone pressed Ctrl+C |
134 | Killed by SIGABRT (6) | The process aborted itself. In Node, this is how the JavaScript heap running out ends: FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory |
137 | Killed by SIGKILL (9) | Killed forcibly from outside. Most often the container or kernel OOM killer — the process used more memory than its limit. Also kill -9, or an orchestrator that gave up waiting for a graceful stop |
139 | Killed by SIGSEGV (11) | Segmentation fault — a crash in native code (a native addon or the runtime itself), not your JavaScript |
143 | Killed by SIGTERM (15) | Asked politely to stop. Normal during deploys and scale-downs. The app should catch SIGTERM, finish in-flight requests and exit |
134 versus 137 matters. 134 means the V8 heap hit its limit (--max-old-space-size) — a JavaScript memory problem. 137 means the whole process exceeded the container's memory limit — which can include native memory, buffers and the heap together. In Kubernetes, the pod status shows OOMKilled for the second case.
Two more terms you will hear around containers:
- CrashLoopBackOff (Kubernetes) — the container keeps crashing on startup, so Kubernetes waits longer and longer between restarts. The cause is in the logs of the previous crashed container (
kubectl logs --previous). - Graceful shutdown — handling
SIGTERMby stopping new work, finishing current requests and closing connections. Without it, every deploy cuts off in-flight requests, which shows up as a burst of502s andECONNRESETs at deploy time.
Part 7: Database Error Codes — PostgreSQL SQLSTATE
Postgres reports every error with a five-character SQLSTATE code (in Node, err.code). The first two characters are the class, and the class alone tells you a lot.
| Class | Meaning | Retry? |
|---|---|---|
08 | Connection exception | Maybe, with backoff — the connection failed |
22 | Data exception — the value is invalid | No — fix the input |
23 | Integrity constraint violation | No — it is a logic or data problem |
25 | Invalid transaction state | No — fix the transaction handling |
28 | Authentication failed | No — fix credentials |
40 | Transaction rollback | Yes — retry the whole transaction |
42 | Syntax error or access rule violation | No — fix the query or schema |
53 | Insufficient resources | Back off |
57 | Operator intervention (cancelled, shutting down) | Depends |
The specific codes you will actually meet:
| SQLSTATE | Name | Typical message | What it means |
|---|---|---|---|
23505 | unique_violation | duplicate key value violates unique constraint "users_email_key" | A row with that unique value already exists — a duplicate signup, or a retried request that already succeeded |
23503 | foreign_key_violation | insert or update on table "orders" violates foreign key constraint | You referenced a row that does not exist (or deleted one still referenced) |
23502 | not_null_violation | null value in column "user_id" violates not-null constraint | A required value was missing — often an undefined from your code |
23514 | check_violation | new row … violates check constraint | The value broke a rule the schema enforces |
22P02 | invalid_text_representation | invalid input syntax for type uuid: "undefined" | The value could not be converted to the column type — very often the literal string "undefined" from JavaScript |
22001 | string_data_right_truncation | value too long for type character varying(50) | The string is longer than the column allows |
25P02 | in_failed_sql_transaction | current transaction is aborted, commands ignored until end of transaction block | An earlier statement in this transaction failed; every statement after it is refused. The real error is the one before this |
40001 | serialization_failure | could not serialize access due to concurrent update | A concurrent transaction conflicted under strict isolation. Retry the transaction |
40P01 | deadlock_detected | deadlock detected | Two transactions waited on each other. Retry, and fix the lock order — see PostgreSQL Ch 15 |
55P03 | lock_not_available | canceling statement due to lock timeout | The statement waited longer than lock_timeout for a lock |
57014 | query_canceled | canceling statement due to statement timeout | The query ran longer than statement_timeout |
53300 | too_many_connections | sorry, too many clients already / remaining connection slots are reserved… | The database's connection limit is reached — usually too many app instances, each with its own pool |
42P01 | undefined_table | relation "orders" does not exist | Wrong table name, wrong schema, or a migration that has not run in this environment |
42703 | undefined_column | column "status" does not exist | Same — a typo, or a missing migration |
28P01 | invalid_password | password authentication failed for user "app" | Wrong credentials for this environment |
3D000 | invalid_catalog_name | database "app_prod" does not exist | Wrong database name in the connection string |
57P01 | admin_shutdown | terminating connection due to administrator command | The database restarted or failed over and dropped your connection |
Some database errors come from the client library or pool, not from Postgres, so they have no SQLSTATE: timeout exceeded when trying to connect (the pool had no free connection in time — usually pool exhaustion), and Connection terminated unexpectedly (the server or network dropped the connection). These are covered in Chapter 9.
Part 8: From Code to Layer — The Reflex
The point of this chapter is one reflex: the code tells you which layer to look at.
| You see | Look at | Not at |
|---|---|---|
ENOTFOUND, EAI_AGAIN, ERR_NAME_NOT_RESOLVED | DNS, hostnames, env vars, which network you are on | Your business logic |
ECONNREFUSED | Is the service running? Right port? Listening on the right interface? | Firewalls (refused means you reached the machine) |
ETIMEDOUT | Firewalls, security groups, routing, an overloaded target | The service's code |
ECONNRESET, socket hang up, 502 | Crashes, restarts, proxies, keep-alive and idle timeouts | Request validation |
| Certificate codes | The certificate, its chain, the hostname, the clock | The network |
Exit 137 / OOMKilled | Container memory limit and the process's total memory | Your error handling |
Exit 134 with "heap out of memory" | JavaScript memory growth — leaks, huge payloads | The container limit |
Exit 143 | Normal shutdown — but check your app handles SIGTERM gracefully | Crashes |
SQLSTATE 23xxx | Your data and your logic (duplicates, missing references) | The database server |
SQLSTATE 40xxx | Concurrency — retry, then reduce contention | Query syntax |
SQLSTATE 42xxx | The query and the schema — and whether migrations ran | Load |
SQLSTATE 25P02 | The previous error in the same transaction | This statement |
The Causal Chains
- If you see
ECONNREFUSED - If you see
ETIMEDOUT - If you see
ENOTFOUND - If you see occasional
ECONNRESETon reused connections - If an HTTPS call works in the browser but fails from Node with
UNABLE_TO_VERIFY_LEAF_SIGNATURE - If a container exits with code
137 - If Node exits with
134and "heap out of memory" - If you see
current transaction is aborted, commands ignored - If Postgres says
invalid input syntax for type uuid: "undefined" - If you see
23505 unique_violationon a create endpoint - If you see
40001or40P01 - If you see
Cannot set headers after they are sent to the client
The Rule You Keep Forever
A code is a precise statement about which layer failed and how. Read the code first, decide the layer, and only then open the code that lives in that layer.
Vocabulary Map
| Term | What it means |
|---|---|
| System call (syscall) | A request from a program to the operating system — open, connect, listen, read, write |
| errno | The number the OS returns when a syscall fails, with a symbolic name like ECONNREFUSED |
err.code | The machine-readable error identifier Node and libraries attach to errors |
| DNS resolution | Turning a hostname into an IP address |
| TCP connect | Opening a connection to an IP and port |
| TCP reset (RST) | An abrupt "this connection is closed" signal; surfaces as ECONNRESET |
| Connection refused | The host answered but nothing listens on the port |
| Connection timeout | Nothing answered within the time limit |
| Keep-alive | Reusing one TCP connection for several HTTP requests |
| Idle timeout | How long a side keeps an unused connection before closing it |
| TLS handshake | The negotiation after TCP connect that verifies the certificate and sets up encryption |
| Certificate chain | The server's certificate plus the intermediate certificates linking it to a trusted root |
| Exit code | The number a process reports when it ends; 0 is success |
| Signal | An OS message to a process — SIGTERM (please stop), SIGKILL (stop now), SIGINT (Ctrl+C) |
| OOM killer | The kernel or container runtime killing a process that exceeded its memory limit |
| OOMKilled | Kubernetes' status for a container killed for exceeding memory |
| CrashLoopBackOff | Kubernetes repeatedly restarting a crashing container with growing delays |
| Graceful shutdown | Handling SIGTERM by finishing in-flight work before exiting |
| SQLSTATE | Postgres' five-character error code; the first two characters are the class |
| Retryable error | A failure that may succeed if repeated unchanged, like 40001 or a transient network error |
Practice Problems
Problem 1 — Refused or timed out? Your app cannot reach Redis. In one environment the error appears instantly as ECONNREFUSED; in another it appears after 10 seconds as ETIMEDOUT. For each, name two likely causes and the first command you would run.
Problem 2 — The container question. A Node API in a container is restarted every few hours. The orchestrator shows exit code 137. A teammate wants to raise --max-old-space-size. Is that the right first move? What evidence would tell you whether the problem is the JS heap or total process memory?
Problem 3 — Read the transaction. Logs show current transaction is aborted, commands ignored until end of transaction block thousands of times, all from one endpoint. What is the very next thing you search the logs for, and why?
Problem 4 — Retry or not? For each, say whether an automatic retry is correct and why: (a) 40P01 deadlock_detected; (b) 23505 unique_violation on POST /signup; (c) ECONNRESET on a GET; (d) ECONNRESET on POST /payments.
Problem 5 — Same error, two worlds. A partner's HTTPS API works in Chrome but your Node service gets UNABLE_TO_VERIFY_LEAF_SIGNATURE. Explain why the behaviour differs, and what you would tell the partner.
What's Coming Next
Most of your debugging traffic speaks HTTP, and HTTP has its own codes: 401, 403, 429, 502, 504. Chapter 4 decodes the status codes — and teaches the skill most engineers miss: working out which hop in the chain actually sent the status. A 502 from your load balancer and a 502 from your own code mean completely different things.