Shutdown & flushing
FileLog and JSONFileLog buffer entries in memory before writing them to disk. This keeps the logger fast and non-blocking — but it means an ungraceful shutdown can drop the last few entries. This page covers when writes happen, what can be lost, and how to guarantee durability.
When does a write hit the disk?
Section titled “When does a write hit the disk?”For FileLog and JSONFileLog, every log() call adds an entry to an in-memory buffer. A write is triggered when any of these hold:
- The buffer reaches
maxMessagesToWrite(default 100). - More than 5 seconds have elapsed since the last write — checked both immediately after each
log()call and by a 5-second background interval. - You call
log.flushSync()explicitly.
The first two are asynchronous (Node streams). The third is synchronous (fs.appendFileSync).
What can be lost?
Section titled “What can be lost?”Anything in the buffer when the process terminates without a synchronous flush:
| Exit path | Buffered entries lost? |
|---|---|
Normal process.exit() | Yes — unless you called log.flushSync() first |
| Uncaught exception without a handler | Yes |
SIGINT / SIGTERM without a handler | Yes |
OS-level kill (kill -9, OOM) | Yes — unavoidable |
await log.info(...) then process.exit() | Yes — log.info resolves once the entry is buffered, not written |
The asynchronous 5-second flush provides eventual durability while the process is alive. It does not help at shutdown.
Graceful shutdown — the easy way
Section titled “Graceful shutdown — the easy way”Pass autoFlushOn to configure() and the logger installs the handlers for you:
import { log, ConsoleLog, FileLog } from "@warlock.js/logger";
log.configure({ channels: [new ConsoleLog(), new FileLog({ chunk: "daily" })], autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"],});Every buffered channel drains before the process exits. You can also call log.enableAutoFlush([...]) directly at any point, and log.disableAutoFlush() to remove every installed handler. Calling enableAutoFlush twice replaces the previous handlers (no stacking).
What each event does:
| Event | Behavior |
|---|---|
SIGINT / SIGTERM / SIGHUP / SIGBREAK / SIGUSR2 | Flush, then re-raise the signal so Node’s default exit behavior runs (exit code 128 + signal number, same as if you had no handler). |
beforeExit | Flush in place. Node exits on its own afterwards — no re-raise. |
Graceful shutdown — the manual way
Section titled “Graceful shutdown — the manual way”If you need to run async work before flushing (closing an HTTP server, finishing an outbound batch), register your own handler and call log.flushSync() at the end:
import { log } from "@warlock.js/logger";
async function gracefulShutdown() { await httpServer.close(); log.flushSync(); process.exit(0);}
process.once("SIGINT", gracefulShutdown);process.once("SIGTERM", gracefulShutdown);flushSync() is fan-out: it calls flushSync() on every registered channel that implements it. ConsoleLog has no buffer and is skipped; FileLog and JSONFileLog each drain to disk. Channels without flushSync are ignored silently.
Shutdown with captured unhandled errors
Section titled “Shutdown with captured unhandled errors”If you use captureAnyUnhandledRejection(), include "beforeExit" in autoFlushOn so the terminal error entry reaches disk before Node tears down:
import { log, ConsoleLog, FileLog, captureAnyUnhandledRejection,} from "@warlock.js/logger";
log.configure({ channels: [ new ConsoleLog(), new FileLog({ chunk: "daily", levels: ["error", "warn"] }), ], autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"],});
captureAnyUnhandledRejection();Without "beforeExit" (or an equivalent manual handler), a crash logs the error into the buffer, then the process exits before the 5-second flush interval fires.
When flushSync is a no-op
Section titled “When flushSync is a no-op”Channels that don’t buffer (like ConsoleLog) skip the call. If every registered channel is unbuffered, flushSync() returns immediately — safe to call unconditionally. Within a FileLog / JSONFileLog, flushSync() is also a no-op when the buffer is empty, so you can schedule it liberally without redundant writes.
See also
Section titled “See also”- Capturing unhandled errors — wire unhandled rejections + uncaught exceptions through the logger
- Custom Channel — implement
flushSyncin your own buffered channel