Skip to content

process.exit() does not abandon in-flight async fs work: 5 fire-and-forget appendFile calls land 5 records where node lands 0 #9442

Description

@proggeramlug

Divergence in the opposite direction from the usual truncation reports — perry writes data node discards.

// five fire-and-forget writes, then exit in the same tick
for (let i = 0; i < 5; i++) fs.promises.appendFile(p, `line ${i}\n`);
process.exit(0);
node : 0 records
perry: 5 records

Node's process.exit() terminates without draining pending libuv work, so promises that have not yet performed their write are simply abandoned. Perry completes them.

Why it matters

process.exit() is the documented way to stop now, and real code relies on that: an error path that exits deliberately should not have partially-written or duplicate state committed behind it. Writing more than node is not a benign difference — it can commit records a program deliberately abandoned, and it means the same code produces different on-disk state under the two runtimes.

It is also a plausible source of confusion in the other direction: anyone debugging a truncation bug will see perry writing more here and may draw the wrong conclusion about which engine buffers.

Scope to establish

Whether this is specific to fs.promises.appendFile or general to pending async work at exit. Worth checking the callback form, createWriteStream, pending timers, pending sockets, and child_process writes — the fix differs a lot depending on whether one subsystem drains on shutdown or the exit path drains everything.

Note the process.on('exit') handler semantics are already known-divergent (recorded during the pi work: not fired on explicit exit), so check whether these share a root before fixing either.

Verification bar

A gap fixture byte-compared to node --experimental-strip-types, demonstrated failing on a compiler built from unfixed origin/main, covering: fire-and-forget promise writes, the callback form, createWriteStream with unflushed data, a pending setTimeout, and an awaited write that has genuinely completed before the exit (which must still land in both engines — that is the control that keeps a fix from over-abandoning).

Found by the differential stress-test of claude-code under perry, while falsifying #9421's async-flush premise.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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