← Volver al blog

Guía

EADDRINUSE en Express: el framework no te ayuda, así que manéjalo tú

14 de septiembre de 20263 min de lectura

Error: listen EADDRINUSE: address already in use :::3000
    at Server.setupListenHandle [as _listen2] (node:net:1872:16)
    ...

Express es una capa delgada sobre el servidor http de Node, y no maneja esto por ti. Sin pregunta como Create React App, sin cambio automático como Next.js o Vite. Recibes el error crudo de Node y un stack trace que apunta a node:net, que no te dice nada sobre tu propio código.

Se puede argumentar que es el comportamiento correcto. También es poco útil a las tres de la tarde.

El arreglo inmediato

lsof -i :3000

Y después:

lsof -ti :3000 | xargs kill

-t imprime solo los IDs de proceso, xargs se los pasa todos a un kill, y un kill normal manda SIGTERM para que el proceso pueda cerrar sus conexiones. Agrega -9 solo si se niega.

Eso resuelve el síntoma. El resto de esto es para no toparte con él todos los días.

Por qué nodemon lo empeora

nodemon reinicia tu app cada vez que cambia un archivo. Le manda una señal al proceso viejo y levanta uno nuevo, y si el nuevo intenta ocupar el puerto antes de que el viejo lo haya soltado, corres una carrera contra ti mismo.

Guarda un archivo dos veces rápido, o guarda mientras el servidor todavía está partiendo, y te llega un EADDRINUSE por un conflicto que creaste hace medio segundo.

Si te pasa seguido, dale un respiro a nodemon:

{
  "delay": "500"
}

en nodemon.json. Medio segundo suele ser suficiente, y no te cuesta nada que vayas a notar.

Maneja el error en vez de leer stack traces

Express no lo va a hacer, pero el objeto del servidor emite el error y lo puedes capturar. Unas pocas líneas convierten una traza fea en algo accionable:

const server = app.listen(PORT)

server.on('error', (err) => {
  if (err.code === 'EADDRINUSE') {
    console.error(`El puerto ${PORT} ya está en uso.`)
    console.error(`Encuéntralo con:  lsof -i :${PORT}`)
    process.exit(1)
  }
  throw err
})

Vale la pena agregarlo en cualquier proyecto donde trabaje más de una persona. Quien se tope con esto a las 3 de la tarde en su primera semana recibe una frase en vez de una traza hacia node:net.

Lee el puerto del entorno

Dejar el 3000 fijo en el código garantiza choques entre proyectos, y además rompe el despliegue, porque la mayoría de los hosts asigna el puerto por variable de entorno.

const PORT = process.env.PORT || 3000

Así, un proyecto que choca solo necesita:

PORT=3001 npm run dev

Sin cambiar código, y sin discutir de quién es el 3000.

El :::3000 del mensaje

Esos tres dos puntos no son un error de tipeo. :: es la dirección comodín de IPv6, así que :::3000 significa “todas las interfaces IPv6, puerto 3000”. Express se ata a ambas pilas por defecto, que es también la razón por la que un solo servidor Express aparece dos veces en un listado de puertos, una por IPv4 y otra por IPv6.

Un servidor, dos entradas. Conviene saberlo antes de salir a cazar un duplicado que no existe. Más sobre eso en qué es realmente localhost.

Cuando lsof no muestra nada

Te llega EADDRINUSE, lsof -i :3000 no imprime absolutamente nada, y empiezas a dudar de las herramientas. El puerto está en TIME_WAIT, reservado por TCP unos treinta segundos después de que una conexión se cierra. Ningún proceso lo posee, así que no hay nada que matar.

La mayoría de los frameworks activa SO_REUSEADDR, por eso esto es raro y no constante. Explicación completa en qué significa realmente EADDRINUSE.

Bosun muestra qué está ocupando el puerto en la barra de menú, así la respuesta ya está en pantalla cuando aparece la traza. macOS 14 o posterior, prueba de 14 días, sin cuenta.

Ve esto en vez de escribirlo

Bosun vive en tu barra de menú y muestra cada puerto abierto en tu Mac, en vivo, mapeado al proceso detrás. Un clic para matarlo, SIGTERM primero. Útil la primera vez que pasa esto. Realmente útil la quinta vez que pasa en una misma tarde.