Skip to content

fix(logger): prevent "write after end" error during shutdown#2599

Open
mldangelo wants to merge 1 commit intowinstonjs:masterfrom
mldangelo:fix/write-after-end-race-condition
Open

fix(logger): prevent "write after end" error during shutdown#2599
mldangelo wants to merge 1 commit intowinstonjs:masterfrom
mldangelo:fix/write-after-end-race-condition

Conversation

@mldangelo
Copy link
Copy Markdown
Contributor

@mldangelo mldangelo commented Dec 11, 2025

Summary

Fixes the "write after end" race condition that occurs when calling logger.end() during heavy logging.

This is a companion fix to #2594 (which addressed File transport flushing). While #2594 fixed data loss at the transport level, this PR fixes a race condition at the Logger level that can cause "write after end" errors.

Problem

When logger.end() is called, the _final() method immediately calls transport.end() on all transports. However, the Logger's readable buffer may still contain data that needs to be piped to transports. Since transport.end() sets the transport's state.ending = true, any subsequent pipe writes fail with "write after end".

Reproduction scenario:

  1. Log many messages rapidly to fill the internal buffer
  2. Call logger.end() immediately
  3. The pipe from Logger to transport races with _final() calling transport.end()
  4. Error: "write after end"

Solution

The fix uses an event-driven approach to ensure the readable buffer is fully drained before ending transports:

  1. When _final() is called, check if the readable buffer is empty
  2. If not empty, listen for 'data' events which fire as the pipe consumes data from the buffer
  3. When the buffer empties (this._readableState.length === 0), remove the listener and end transports
  4. If buffer is already empty, proceed immediately

This approach is:

  • Event-driven - Only checks when data is actually consumed, not polling on every tick
  • No arbitrary limits - Trusts the Node.js stream mechanism instead of magic numbers
  • More efficient - Fewer function calls, no timer management

Testing

Added new test file test/unit/winston/write-after-end.test.js with 10 comprehensive test cases:

  1. No "write after end" errors during heavy logging (1000 messages)
  2. All messages are flushed when ending immediately after logging
  3. Readable buffer drains before transports end
  4. Multiple file transports work without errors
  5. Empty buffer handled correctly
  6. Rapid successive log + end calls work
  7. Mixed Console + File transports work
  8. Event-driven drain path with slow transport (exercises the onData listener)
  9. Backpressure handling with slow transport (guarantees buffer has data when _final runs)
  10. Mixed fast and slow transports (tests real-world scenario)

Tests 8-10 use a custom SlowTransport to simulate backpressure and ensure the event-driven drain path is exercised (not just the fast "buffer already empty" path).

All 244 existing tests pass.

Related Issues

Fixes #2219

Related to #2594 - Together these two fixes ensure reliable logging shutdown: #2594 ensures File transport data reaches disk, and this PR ensures all buffered data reaches transports before they end.

Checklist

  • Tests pass locally (244 passed)
  • Added comprehensive tests including slow transport scenarios
  • Event-driven drain path has test coverage
  • No breaking changes
  • No arbitrary timeouts or iteration limits

@mldangelo mldangelo force-pushed the fix/write-after-end-race-condition branch 2 times, most recently from 79d17e0 to e393f86 Compare December 11, 2025 17:01
When logger.end() is called during heavy logging, a race condition can
occur where the readable buffer still contains data when transports are
ended, causing 'write after end' errors.

The issue is in _final(): the pipe from Logger to transports is
asynchronous - data pushed via _transform() goes to the readable buffer,
and the pipe reads from it to write to transports. If transport.end()
is called while the readable buffer still has data, the pipe tries to
write to an ending transport.

The fix uses an event-driven approach: listen for 'data' events which
fire as the pipe consumes data from the readable buffer. When the buffer
empties, we can safely end transports. This is more efficient than
polling and trusts the Node.js stream mechanism.

Fixes: winstonjs#2219
@mldangelo mldangelo force-pushed the fix/write-after-end-race-condition branch from e393f86 to 569d0ba Compare December 11, 2025 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Write after end node error with winston logger

1 participant