Skip to content

nest start --watch hangs on SIGINT since v11.0.17 #3391

Description

@Segfaultd

Did you read the migration guide?

  • I have read the whole migration guide

Is there an existing issue that is already proposing this?

  • I have searched the existing issues

Potential Commit/PR that introduced the regression

The signal handler was added somewhere between v11.0.16 and v11.0.17 in actions/start.action.js.

Versions

11.0.16 -> 11.0.17 (also affects 11.0.18, 11.0.19)

Describe the regression

Since @nestjs/[email protected], nest start --watch hangs after receiving SIGINT (Ctrl+C). The child NestJS application exits cleanly (port is freed, shutdown hooks complete), but the CLI process itself stays alive indefinitely. A second Ctrl+C is required to exit.

A signal handler was added in v11.0.17 (not present in v11.0.16) in actions/start.action.js:

const signalHandler = (signal) => {
    if (childProcessRef) {
        childProcessRef.kill(signal);
    }
    else {
        process.exit();
    }
};
process.on('SIGINT', signalHandler);
process.on('SIGTERM', signalHandler);

When SIGINT is received:

  1. The handler forwards the signal to the child process: childProcessRef.kill(signal)
  2. The child exits and the exit handler sets childProcessRef = undefined
  3. But the CLI process itself never exits — the file watcher (chokidar/tsc) keeps the event loop alive
  4. On a second Ctrl+C, childProcessRef is undefined, so process.exit() is finally called

In v11.0.16, this signal handler did not exist. SIGINT was delivered directly to the process group, the child process terminated via the default handler, and the CLI process also exited naturally via the default SIGINT behavior.

Minimum reproduction code

# Any NestJS project
npx @nestjs/[email protected] start --watch
# Press Ctrl+C → process hangs
# Press Ctrl+C again → process finally exits

# Compare with:
npx @nestjs/[email protected] start --watch
# Press Ctrl+C → exits immediately

Expected behavior

nest start --watch should exit on a single Ctrl+C, as it did in v11.0.16.

A possible fix — exit the CLI when the child exits after a signal was received:

let shuttingDown = false;
const signalHandler = (signal) => {
    shuttingDown = true;
    if (childProcessRef) {
        childProcessRef.kill(signal);
    } else {
        process.exit();
    }
};
// ...
childProcessRef.on('exit', (code) => {
    process.exitCode = code;
    childProcessRef = undefined;
    if (shuttingDown) {
        process.exit(code ?? 0);
    }
});

Other

  • OS: macOS 25.3.0 (arm64)
  • Node.js: v24.14.0
  • Package manager: pnpm 10.32.1
  • Builder: Both tsc and swc are affected (the signal handler is in start.action.js, shared by all builders)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions