Guide
Express EADDRINUSE: the framework gives you nothing, so handle it yourself
September 14, 20263 min read
Error: listen EADDRINUSE: address already in use :::3000
at Server.setupListenHandle [as _listen2] (node:net:1872:16)
...
Express is a thin layer over Node’s http server, and it does not handle this for you. No prompt like Create React App, no automatic move like Next.js or Vite. You get the raw Node error and a stack trace pointing into node:net, which tells you nothing about your own code.
That is arguably correct behaviour. It is also unhelpful at three in the afternoon.
The immediate fix
lsof -i :3000
Then:
lsof -ti :3000 | xargs kill
-t prints only the process IDs, xargs passes all of them to one kill, and plain kill sends SIGTERM so the process can close its connections. Add -9 only if it refuses.
That resolves the symptom. The rest of this is about not meeting it every day.
Why nodemon makes this worse
nodemon restarts your app whenever a file changes. It sends a signal to the old process and starts a new one, and if the new process binds before the old one has released the port, you race yourself.
Save a file twice quickly, or save while the server is still starting, and you get EADDRINUSE from a conflict you created half a second ago.
If this happens often, give nodemon a moment:
{
"delay": "500"
}
in nodemon.json. Half a second is usually plenty, and it costs you nothing you will notice.
Handle the error instead of reading stack traces
Express will not do it, but the server object emits the error and you can catch it. A few lines turn an ugly trace into something actionable:
const server = app.listen(PORT)
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`Port ${PORT} is already in use.`)
console.error(`Find it with: lsof -i :${PORT}`)
process.exit(1)
}
throw err
})
Worth adding to any project more than one person works on. The person who hits it at 3pm on their first week gets a sentence instead of a stack trace into node:net.
Read the port from the environment
Hardcoding 3000 guarantees collisions across projects, and it also breaks deployment, since most hosts assign a port through the environment.
const PORT = process.env.PORT || 3000
Then a colliding project just needs:
PORT=3001 npm run dev
No code change, no argument about who owns 3000.
The :::3000 in the message
Those three colons are not a typo. :: is the IPv6 wildcard address, so :::3000 means “every IPv6 interface, port 3000”. Express binds both stacks by default, which is also why a single Express server often shows up twice in a port listing, once for IPv4 and once for IPv6.
One server, two entries. Worth knowing before you go hunting a duplicate that does not exist. More on that in what localhost really is.
When lsof shows nothing
You get EADDRINUSE, lsof -i :3000 prints nothing at all, and you start doubting the tools. The port is in TIME_WAIT, reserved by TCP for around thirty seconds after a connection closes. No process owns it, so there is nothing to kill.
Most frameworks set SO_REUSEADDR, which is why this is rare rather than constant. Full explanation in what EADDRINUSE actually means.
Bosun shows what is holding the port in the menu bar, so the answer is already on screen when the trace appears. macOS 14 or later, 14 day trial, no account.
See this instead of typing it
Bosun lives in your menu bar and shows every open port on your Mac, live, mapped to the process behind it. One-click kill, SIGTERM first. Useful the first time this happens. Genuinely useful the fifth time it happens in one afternoon.