Node.js / Editorial solution guide
EADDRINUSE: diagnose the listener and close what you own
Find why a local HTTP bind fails, distinguish a port conflict from a lifecycle leak, and verify a bounded repair without terminating unrelated processes.
Read-only knowledge base · No public comments
Recognize the symptoms
Startup emits an Error with code EADDRINUSE before the server becomes ready. Tests may pass individually but fail together, or a terminal may stay occupied after the test result appears. A browser response from the expected port can come from an older process, so seeing a page does not prove that the new instance started.
Understand the likely causes
- Another listener already owns the requested address and port, a previous test never closed its server, or the same application started twice. Binding all interfaces and binding a specific interface can also interact depending on the operating system. EADDRINUSE identifies a bind conflict; it does not establish which process should be stopped.
Confirm the exact failed bind
Read the error code, address, and port from the failed startup and compare them with the intended configuration. Confirm which command and working directory launched the instance and whether a development watcher already manages another child. Use your operating system’s read-only listener inspection tool to identify an owner, then inspect that owner’s command and project context. Do not terminate an unknown process merely because its port matches. If it is your development instance, stop it through its existing terminal or supervisor so cleanup can run normally.
Reproduce only inside an owned lab
The reproduction reserves an ephemeral loopback port, deliberately asks a second owned server to bind that same address, observes EADDRINUSE, and releases the first server in finally. It never targets a real service. The second server did not begin listening, so there is no successful second listener to close. This isolates the mechanism from assumptions about a particular fixed port. A real application should attach startup error handling before announcing readiness and report the conflict clearly rather than continuing in a half-started state.
import { createServer } from 'node:http';
import { once } from 'node:events';
const owner = createServer((req, res) => res.end('lab'));
owner.listen(0, '127.0.0.1'); await once(owner, 'listening');
try {
const competing = createServer();
const failed = once(competing, 'error');
competing.listen(owner.address().port, '127.0.0.1');
const [error] = await failed;
console.log(error.code);
} finally { await new Promise((resolve, reject) => owner.close(e => e ? reject(e) : resolve())); }
EADDRINUSE
Both servers belong to this short-lived demonstration; the conflict occurs only on a local ephemeral port.
Repair ownership and configuration
For parallel tests, select port zero and read server.address().port after the listening event. Keep the chosen port in that test’s scope and pass it to its client. For a deployed service, select the documented configured port and resolve the actual owner through the deployment supervisor; switching to an arbitrary port can break routing and health checks. Remove duplicate startup paths and ensure importing a helper does not start a listener as a side effect. The right repair depends on whether this is test isolation, duplicate startup, or intentional service configuration.
Close on every outcome
The repair example closes its server even if the client fails. A long-running service additionally needs an idempotent drain sequence, a deadline, and closure of workers, pools, timers, and upgraded connections. Closing the HTTP listener alone does not prove the process has no remaining resources. Avoid immediate process.exit as a substitute for ownership; it can truncate responses and conceal leaks. A development watcher can restart a child before the old instance drains, so coordinate its stop signal and restart behavior with the application lifecycle.
import { createServer } from 'node:http';
import { once } from 'node:events';
const server = createServer((req, res) => res.end('owned'));
server.listen(0, '127.0.0.1'); await once(server, 'listening');
try {
const response = await fetch(`http://127.0.0.1:${server.address().port}`);
console.log(await response.text());
} finally { await new Promise((resolve, reject) => server.close(e => e ? reject(e) : resolve())); }
console.log(server.listening);
owned
false
The response completes and finally releases the listener. The final false confirms this server is no longer listening.
Verify the fix
Run each lab script and confirm its expected output and natural process exit. Re-run the formerly conflicting test alone and with its relevant neighbors, checking that every successful listen has one cleanup owner. For an actual service, verify the documented readiness response from the intended new process and confirm routing still uses the configured port.
Keep it from recurring
- Use ephemeral ports for isolated tests, register cleanup immediately, and keep startup separate from imports. Document the service manager, configured bind address, and graceful stop command. Treat a port conflict as evidence to investigate rather than a reason to kill every Node process.