Appearance
Promisified Server Startup
Wraps the event-based server.listen() API in a promise and returns a structured object with url, port, and listener. Includes development-mode retry logic for automatic port conflict resolution.
When to Use
- Starting an Express (or any Node.js HTTP) server with
await - Needing the URL, port, and server handle as a clean return value
- Handling
EADDRINUSEgracefully during development - Setting up graceful shutdown with
listener.close()
The Pattern
The listen() helper wraps server startup in a promise and returns a structured object with url, port, and listener:
javascript
const { url, listener } = await listen(app, 3000);
console.log(`Running at ${url}`);Graceful shutdown
Use the returned listener to close the server during cleanup:
javascript
const { url, listener } = await listen(app, port);
console.log(`Server running at ${url}`);
cleanupOnExit(async function cleanup() {
if (cleanup.complete) return;
await listener.close();
process.kill(process.pid, "SIGHUP");
cleanup.complete = true;
});With auto port selection
Combine with a port finder for conflict-free startup:
javascript
const port = await getPort(8000);
const { url } = await listen(app, port);
console.log(`Server running at ${url}`);Implementing listen()
javascript
import { createServer } from "node:http";
export async function listen(app, port) {
const listener = createServer(app);
return startServer();
async function startServer() {
try {
await new Promise((resolve, reject) => {
listener.listen(port);
listener.once("listening", resolve);
listener.once("error", reject);
});
const url = `http://localhost:${port}`;
return { url, port, listener };
} catch (error) {
if (DEVELOPMENT && error.code === "EADDRINUSE") {
console.warn(
`Port ${port} in use, retrying...`,
);
await clearPort(port);
await delay(1000);
return startServer();
}
throw error;
}
}
}Return value
The structured return gives each consumer exactly what it needs:
url— for display:console.log(url)port— for configuration or passing to other serviceslistener— for lifecycle management:await listener.close()
Event-to-promise bridge
The core technique wraps Node's event-based API:
javascript
await new Promise((resolve, reject) => {
listener.listen(port);
listener.once("listening", resolve);
listener.once("error", reject);
});This is a general pattern for any event emitter that signals success with one event and failure with another.
Development-mode retry
In development, port conflicts are common. The retry logic kills the blocking process and tries again:
javascript
async function clearPort(port) {
try {
await sh`lsof -ti tcp:${port} | xargs kill -9`
.run();
} catch (error) {
console.error(
`Failed to clear port ${port}:`,
error,
);
}
}This only runs in development — production should fail immediately on port conflicts.
Trade-offs
| Approach | Pros | Cons |
|---|---|---|
Promisified listen() | Awaitable, structured | Extra wrapper |
app.listen(cb) | Built-in, simple | Callback-based |
| Event listeners | Full control | Verbose |
The structured return is the key value. Without it, you store the port in a variable and construct the URL by hand every time. The { url, port, listener } object gives every consumer a clean interface.