# winston: a multi-transport logger for Node.js

> winston is an MIT-licensed JavaScript logging library built around transports, logform formats and RFC5424 severity levels. It suits Node services that need per-level routing to files, console or remote storage, and it costs you a logger object you configure yourself.

**winstonjs/winston** — A logger for just about everything.

- Repository: https://github.com/winstonjs/winston
- Website: http://github.com/winstonjs/winston
- Stars: 24,518 · Forks: 1,851
- Language: JavaScript
- License: MIT
- Published: 2026-09-21 · Updated: 2026-09-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/winstonjs-winston

## What winston solves, and who ends up configuring it

Node's console gives you one output stream and no notion of severity beyond whatever string you prepend. winston's answer is to split logging into three parts that can be changed independently: levels decide what is worth recording, formats decide what an entry looks like, and transports decide where it goes. The README frames a transport as "essentially a storage device for your logs", and a single logger can hold several of them at different levels. That is the whole design thesis, and it is why the library has stayed useful across so many runtimes and deployment styles.

The audience is application authors who already know they want error lines in one file and everything else in another, or who want the same call site to feed a console during development and a remote store in production. If your requirement is simply "print JSON to stdout and let the platform collect it", winston is more machinery than you need. The README is explicit that the recommended path is to build your own logger with winston.createLogger rather than rely on the shared default, which tells you the project expects configuration work from the adopter.

## Levels, formats and transports: the three moving parts

Levels follow the severity ordering in RFC5424, and the README states that severity is assumed to be numerically ascending from most important to least important. The default set is winston.config.npm.levels, which the README prints as error 0, warn 1, info 2, http 3, verbose 4, debug 5, silly 6. Those names become convenience methods on the logger object, so logger.info and logger.error exist without you declaring them. The level option on a logger sets the ceiling: entries at a numerically higher level than the configured one are dropped.

Formats come from logform, a separate dependency. winston.format.json() is the default, and the README shows winston.format.simple() for console output in development. Formats compose, and the documentation covers combining them, string interpolation, filtering info objects and writing custom ones.

Transports are objects such as winston.transports.File and winston.transports.Console, and each can carry its own level, which is how the README's example sends only error and fatal lines to error.log while combined.log receives info and above. The decoupling is real: swapping a transport does not change call sites, and changing a format does not change where bytes land. The cost is that nothing is configured for you. A logger with an empty transports array, which is the default, accepts calls and discards them; the README warns that leaving the default logger without transports may produce a high memory usage issue.

## Install and a first logger that writes two files

winston ships on npm and the package entry point is ./lib/winston.js, with types at ./index.d.ts for TypeScript users. Install it as a normal dependency:

```bash
npm install winston
```

The README's usage section recommends building your own logger rather than logging through the shared default. This configuration sets a level, a JSON format, a default metadata field, and two file transports with different level filters. The first transport receives error and fatal entries only; the second receives info and above.

```js
const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  defaultMeta: { service: 'user-service' },
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' }),
  ],
});
```

After a call such as logger.info('started'), you should find a line in combined.log and nothing in error.log. A logger.error('boom') call writes to both, because error is at or above the second transport's implicit level. The README also shows adding a console transport guarded by a NODE_ENV check so development output stays readable while production stays JSON:

```js
if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple(),
  }));
}
```

The repository carries a matching examples/quick-start.js plus a long list of runnable examples, including custom-transport.js, format-filter.js and file-maxsize.js, which is where to look when the README's prose runs out.

## The default logger and the exitOnError default are the two traps

Two defaults deserve attention before you put winston behind a production service.

The first is the shared logger from require('winston'). It exists for convenience, and the README says it has no transports by default, that you must add them yourself, and that leaving it without any may produce a high memory usage issue. A library that logs through the shared logger without configuring it is silently dropping records. If you maintain a package that depends on winston, create your own logger instead of writing to the default one.

The second is exitOnError, which the README lists with a default of true and the description "If false, handled exceptions will not cause process.exit". A logger that terminates the process on a handled exception is a deliberate choice, and it is the opposite of what many teams expect from a logging library. The README devotes a section to the question of whether to exit, so the behaviour is documented rather than hidden, but it is still a default you should decide about explicitly.

Beyond defaults, the practical limit is scope. winston records what you hand it. It does not sample, it does not deduplicate, and it does not ship logs anywhere on its own unless a transport for that destination exists. Treating it as an observability pipeline rather than a logging library leads to configuration you have to maintain yourself.

## winston against pino and bunyan

The package keywords list pino and bunyan alongside winston, so the project itself places them in the same category. The difference is where the flexibility sits.

pino is built around a fast JSON serializer and a narrower surface: you get structured output and you attach transports through a separate mechanism. If your logs go to stdout and are collected by the platform, pino's defaults ask less of you. winston's defaults ask more, and in exchange you get per-transport level filtering and a format pipeline you can rewrite, which is the configuration the README's two-file example relies on.

bunyan occupies similar ground with its own JSON conventions, and winston's ecosystem includes winston-compat as a dev dependency for compatibility work. The honest distinction is not speed or output shape. It is that winston treats the transport as a first-class object with its own level and format, which makes mixed routing straightforward and makes an unconfigured logger a real hazard.

## Maintenance, licence and the upgrade cost

winston is MIT licensed, which permits commercial use and modification provided the copyright notice and permission notice are retained. That is a summary of the licence text, not legal advice; read LICENSE in the repository if the distinction matters to your organisation.

The repository is not archived, and the last push was on 2026-07-20. Releases are versioned in the 3.x line, with v3.19.0 published on 2025-12-07 and v3.18.3 and v3.18.2 before it on 2025-09-30. The package declares a main entry at ./lib/winston.js and a browser build produced by a Babel step (npm run build runs babel lib -d dist, with a rimraf prebuild), so consumers on bundlers should be aware there are two build outputs.

The upgrade cost is concentrated in the major-version boundary. The README points to UPGRADE-3.0.md for the move to winston@3 and keeps the winston@2.x documentation separate, which means code written against 2.x needs a real read of that guide rather than a version bump. Within 3.x the API surface shown in the README (createLogger, format, transports, levels) has been stable enough that the release cadence is patch and minor work. Dependency weight is another cost: the package pulls in logform, winston-transport, readable-stream, async, safe-stable-stringify, stack-trace and others, so a minimal install is not what you get.

## Conclusion

Adopt winston when you need one logger writing to several destinations at different levels and you are willing to own the configuration. Skip it if you want a zero-config JSON logger or you are not on Node. Before shipping, verify that your default logger has at least one transport, decide whether exitOnError should stay true, and check that your level names match winston.config.npm.levels or your own custom levels.

## FAQ

### How do I install winston in Node.js?

Install it from npm as a regular dependency, then require it and build a logger with winston.createLogger. The package entry point is ./lib/winston.js and TypeScript types ship at ./index.d.ts.

### How do I use the winston logger in Node.js?

Create a logger with winston.createLogger, passing a level, a format and a transports array. Level names such as info and error become methods on the returned logger, so you call logger.info('message') directly.

### Does winston have any transports enabled by default?

No. The default logger exposed by require('winston') has no transports, and the README says you need to add them yourself and that leaving it without any may produce a high memory usage issue.

### What logging levels does winston use out of the box?

The default is winston.config.npm.levels: error, warn, info, http, verbose, debug and silly, ordered by ascending numeric severity per RFC5424. You can supply your own levels object to createLogger instead.

### What does exitOnError do in winston?

It defaults to true, and the README describes it as controlling whether handled exceptions cause process.exit. Setting it to false keeps the process alive after a handled exception.

## Sources

- [License: MIT](https://github.com/winstonjs/winston/blob/master/LICENSE)
- [Project website](http://github.com/winstonjs/winston)
- [README](https://github.com/winstonjs/winston/blob/master/README.md)
- [Releases](https://github.com/winstonjs/winston/releases)
- [winstonjs/winston on GitHub](https://github.com/winstonjs/winston)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/winstonjs-winston
