| | |

ESLint 19 🛡️ Output Formats — stylish, json, and custom formatters

⏱️ Estimated read: 14 min | |

ESLint’s output is not a fixed log. It is a formatter function that receives the lint results and returns a string. The default formatter is stylish, which produces the human-readable, colorized output that most developers recognize: a list of files, the rule violations under each file, and a summary line. The json formatter produces the same data in a machine-readable structure, which is what CI tools, editor integrations, and custom scripts consume. And a custom formatter is any function that takes the same results and returns a different string—a format optimized for a specific tool, a different display style, or a file format that the built-in formatters do not provide .

The formatter is selected with the --format or -f flag. The value can be the name of a built-in formatter (stylish, json, compact, unix, html, and others), a path to a local formatter file (which must begin with .), or the name of an npm package that follows the eslint-formatter-* naming convention . The flag is the only configuration surface. The formatter itself is a function, and the function’s signature and the shape of its arguments are the entire API.

Key point: A formatter is a function that receives a results array (the lint results for each file) and a context object (metadata about the run) and returns a string. The built-in json formatter is literally JSON.stringify(results, null, 2) . A custom formatter is the same function with a different string.


Why the formatter is a function, not a fixed output

The consumer diversity problem. The same lint results are consumed by different tools. A terminal wants colorized, human-readable output. A CI pipeline wants a structured format that a script can parse. An editor integration wants the results in a form that maps to the editor’s diagnostic API. A pull request bot wants a summary that fits in a comment. A single output format cannot serve all of these consumers. The formatter function is the extension point that lets each consumer receive the format it needs without changing ESLint’s core .

The stylish default. The stylish formatter is the default because it is the most useful for interactive development. It groups the results by file, shows the line and column of each problem, and uses color to distinguish errors from warnings. The format is familiar to anyone who has run ESLint in a terminal. The json formatter is the default for tools because it preserves the full structure of the results, including fields that stylish omits, such as the ruleId, the severity, the fix object, and the source code .

The custom formatter extension point. A custom formatter is a function that the developer writes or installs. It receives the same results and context arguments and returns a string. The function can be synchronous or asynchronous (since ESLint v8.4.0). It can use the context object to access the current working directory, the maxWarningsExceeded state, and the rulesMeta information for each rule. It can read environment variables to change its behavior. The function is the entire contract; there is no configuration object and no plugin API beyond the function signature .

The trade-off. A custom formatter is code that must be maintained. The built-in formatters are maintained by the ESLint team, and they are updated when the results structure changes. A custom formatter that depends on a specific field in the results may break when ESLint adds or removes a field. The function signature is stable, but the shape of the results array is not guaranteed to be frozen. The built-in formatters are the safe choice for most projects; a custom formatter is warranted when the output must match a specific tool’s format that the built-ins do not provide.


a. The stylish formatter and the human-readable default

The stylish formatter is the default, and it is the one that most developers see without realizing a formatter is involved. It produces output grouped by file, with the rule violations listed under each file and a summary at the end.

# The default formatter is stylish.
# These two commands produce the same output.
npx eslint src/
npx eslint src/ --format stylish

The output for a file with two problems looks like this:

/path/to/src/app.js
  2:10  error    'addOne' is defined but never used        no-unused-vars
  3:20  warning  Missing semicolon                         semi

✖ 2 problems (1 error, 1 warning)
  1 error and 0 warnings potentially fixable with the `--fix` option.

The stylish format includes the file path, the line and column, the severity (error or warning), the message, and the rule ID. The summary line includes the total counts and, if applicable, the number of problems that --fix could correct. The color is applied when the terminal supports it, and the --color and --no-color flags control it .

The stylish formatter is the correct choice for local development and for any context where a human is reading the output. It is not the correct choice for a script that needs to parse the results, because the format is designed for readability, not for machine consumption.


b. The json formatter and the machine-readable structure

The json formatter produces a JSON array of LintResult objects. Each object contains the file path, the messages, the error and warning counts, the fixable counts, and optionally the source code or the fixed output.

# The json formatter.
# The output is a JSON array that a script can parse.
npx eslint src/ --format json

The json formatter is implemented as a single line:

// The built-in JSON formatter, in its entirety.
module.exports = function (results, context) {
  return JSON.stringify(results, null, 2);
};

The results array contains one object per file. Each object has the following fields :

FieldMeaning
filePathAbsolute path to the file
messagesArray of message objects (rule violations)
errorCountNumber of errors in the file
warningCountNumber of warnings in the file
fixableErrorCountNumber of errors that --fix can correct
fixableWarningCountNumber of warnings that --fix can correct
sourceThe source code (omitted if there are no problems or if output is present)
outputThe fixed source code (omitted if no fixes were applied)

Each message object contains the ruleId, severity (1 for warning, 2 for error), message, line, column, and nodeType . The json formatter is what CI scripts, editor integrations, and custom tools consume. It is the formatter that --fix-dry-run requires to show the corrected output, because the output field is only present in the JSON structure .

// Parsing the json output in a Node.js script.
const { execSync } = require("node:child_process");

const output = execSync("npx eslint src/ --format json", { encoding: "utf8" });
const results = JSON.parse(output);

for (const file of results) {
  if (file.errorCount > 0) {
    console.log(`${file.filePath}: ${file.errorCount} errors`);
  }
}

The json formatter is the bridge between ESLint and any tool that needs structured data. The stylish formatter is for humans; the json formatter is for programs.


c. Writing a custom formatter

A custom formatter is a function that receives the same results and context arguments and returns a string. The function can be local (a file in the project) or distributed as an npm package with the eslint-formatter-* naming convention.

// my-formatter.js — a custom formatter that outputs a summary
module.exports = function (results, context) {
  let totalErrors = 0;
  let totalWarnings = 0;

  for (const result of results) {
    totalErrors += result.errorCount;
    totalWarnings += result.warningCount;
  }

  // The formatter returns a string.
  // This one returns a single summary line.
  return `Lint complete: ${totalErrors} errors, ${totalWarnings} warnings\n`;
};
# Use a local formatter.
# The path must begin with a period.
npx eslint src/ --format ./my-formatter.js

The context argument provides additional information. The cwd is the current working directory. The maxWarningsExceeded is present if --max-warnings was set and the count exceeded the limit; it contains the maxWarnings value and the foundWarnings count. The rulesMeta contains the meta property of each rule that produced a message, which includes the rule’s description, category, and documentation URL .

// A formatter that uses context.rulesMeta to include rule descriptions.
module.exports = function (results, context) {
  const lines = [];

  for (const result of results) {
    for (const message of result.messages) {
      const meta = context.rulesMeta[message.ruleId];
      const description = meta?.docs?.description ?? "unknown rule";
      lines.push(`${result.filePath}:${message.line} ${message.message} (${description})`);
    }
  }

  return lines.join("\n") + "\n";
};

A formatter can be distributed as an npm package. The package name must follow the eslint-formatter-* convention, and the main entry in package.json must point to the formatter function. When the package is installed, the --format flag uses the short name without the prefix: --format awesome resolves to eslint-formatter-awesome .

// package.json for a custom formatter package
{
  "name": "eslint-formatter-awesome",
  "version": "1.0.0",
  "main": "index.js",
  "keywords": ["eslint", "eslint-formatter"]
}

Complete Example Session

// ============================================
// PART 1: THE DEFAULT STYLISH FORMATTER
// ============================================

// npx eslint src/
// This uses stylish by default.
// The output is grouped by file, with line, column,
// severity, message, and rule ID.

// ============================================
// PART 2: THE JSON FORMATTER
// ============================================

// npx eslint src/ --format json
// The output is a JSON array of LintResult objects.
// Each object contains filePath, messages, errorCount,
// warningCount, fixableErrorCount, fixableWarningCount,
// and optionally source or output.

// ============================================
// PART 3: PARSING JSON IN A SCRIPT
// ============================================

// A Node.js script that runs ESLint and parses the JSON.
const { ESLint } = require("eslint");

async function lintAndParse() {
  const eslint = new ESLint();
  const results = await eslint.lintFiles(["src/"]);

  // The results array is the same structure
  // that the json formatter serializes.
  for (const result of results) {
    console.log(result.filePath);
    for (const message of result.messages) {
      console.log(`  ${message.line}:${message.column} ${message.message}`);
    }
  }
}

// ============================================
// PART 4: A CUSTOM SUMMARY FORMATTER
// ============================================

// summary-formatter.js
module.exports = function (results) {
  const total = results.reduce(
    (acc, r) => ({
      errors: acc.errors + r.errorCount,
      warnings: acc.warnings + r.warningCount,
    }),
    { errors: 0, warnings: 0 }
  );

  return `Total: ${total.errors} errors, ${total.warnings} warnings\n`;
};

// npx eslint src/ --format ./summary-formatter.js
// Output: Total: 5 errors, 3 warnings

// ============================================
// PART 5: A CUSTOM FORMATTER USING CONTEXT
// ============================================

// context-formatter.js
module.exports = function (results, context) {
  // The context.cwd is the working directory.
  // The context.maxWarningsExceeded is present if the limit was exceeded.
  // The context.rulesMeta contains the rule metadata.

  const lines = [`Working directory: ${context.cwd}`];

  if (context.maxWarningsExceeded) {
    lines.push(
      `Warning limit exceeded: ${context.maxWarningsExceeded.foundWarnings} > ${context.maxWarningsExceeded.maxWarnings}`
    );
  }

  return lines.join("\n") + "\n";
};

// ============================================
// PART 6: THE ASYNC FORMATTER
// ============================================

// async-formatter.js (ESLint v8.4.0+)
module.exports = async function (results) {
  // The formatter can await an async operation.
  const summary = await generateSummary(results);
  return summary;
};

// ============================================
// PART 7: THE NPM PACKAGE FORMATTER
// ============================================

// eslint-formatter-awesome/index.js
module.exports = function (results) {
  return results
    .filter(r => r.errorCount > 0)
    .map(r => `${r.filePath}: ${r.errorCount} errors`)
    .join("\n");
};

// After installing the package:
// npx eslint src/ --format awesome
//
// The short name is resolved to eslint-formatter-awesome.

// ============================================
// PART 8: THE HTML FORMATTER
// ============================================

// npx eslint src/ --format html > report.html
//
// The html formatter produces an HTML page
// with the lint results.
// It is the only built-in formatter that does not return a plain string;
// it returns a string that is an HTML document.

// ============================================
// PART 9: THE COMPACT AND UNIX FORMATTERS
// ============================================

// npx eslint src/ --format compact
// Output: /path/to/file.js: line 2, col 10, Error - 'addOne' is defined but never used. (no-unused-vars)

// npx eslint src/ --format unix
// Output: /path/to/file.js:2:10: 'addOne' is defined but never used [Error/no-unused-vars]

// Both are single-line-per-problem formats
// that are easier to parse than stylish.

// ============================================
// PART 10: THE VERIFICATION CHECKLIST
// ============================================

// [ ] The --format flag is passed the correct value.
// [ ] For a local formatter, the path begins with a period.
// [ ] For an npm package, the name follows eslint-formatter-*.
// [ ] The formatter function receives results and context.
// [ ] The formatter returns a string (or a Promise<string>).
// [ ] The json formatter is used for machine consumption.
// [ ] The stylish formatter is used for human consumption.

The ten parts covered the default stylish output, the json structure, the parsing pattern, the custom summary formatter, the context argument, the async formatter, the npm package convention, the HTML formatter, the compact and unix formats, and the verification checklist. The through-line is that the formatter is a function, and the function’s arguments are the entire API.


Quick Reference

Built-in formatters

FormatterOutput styleUse case
stylish (default)Colorized, grouped by fileHuman reading
jsonJSON array of LintResultScripts, tools
compactOne line per problemParsing
unixfile:line:col: message [Severity/rule]Unix tools
htmlHTML documentReports

Custom formatter resolution

--format valueResolves to
stylishBuilt-in
jsonBuilt-in
./my-formatter.jsLocal file (path begins with .)
awesomeeslint-formatter-awesome npm package

The formatter signature

ArgumentTypeContains
resultsLintResult[]File paths, messages, counts, source/output
contextobjectcwd, maxWarningsExceeded, rulesMeta
Returnstring | Promise<string>The formatted output

Best Practices

✅ Do This:

// Use json for machine consumption
npx eslint src/ --format json                                   // ✅

// Use stylish for human consumption (the default)
npx eslint src/                                                  // ✅

// Begin a local formatter path with a period
npx eslint src/ --format ./my-formatter.js                      // ✅

// Use the context argument for metadata
module.exports = function (results, context) { /* ... */ };      // ✅

❌ Don’t Do This:

// Parse the stylish output in a script
// The format is for humans, not machines.                        // ❌

// Omit the period for a local formatter
npx eslint src/ --format my-formatter.js // resolves to npm     // ❌

// Assume the results structure is frozen
// Fields can change between ESLint versions.                     // ❌

Common Pitfalls

PitfallWhy It HappensFix
Local formatter is not foundPath does not begin with .Use ./my-formatter.js
npm formatter is not foundPackage name is wrongInstall eslint-formatter-* and use the short name
Formatter returns nothingFunction has no return statementReturn the string
Async formatter failsESLint version < 8.4.0Upgrade ESLint
JSON output is emptyNo files matched the globCheck the file patterns

Real-World Examples

1. Default stylish

npx eslint src/

2. JSON output

npx eslint src/ --format json

3. Local formatter

npx eslint src/ --format ./summary.js

4. npm formatter

npx eslint src/ --format awesome

5. Custom summary

module.exports = (results) => `Total: ${results.length} files\n`;

6. Context usage

module.exports = (results, context) => context.cwd + "\n";

7. Async formatter

module.exports = async (results) => await format(results);

8. Compact formatter

npx eslint src/ --format compact

9. Unix formatter

npx eslint src/ --format unix

10. HTML report

npx eslint src/ --format html > report.html

Visual

The Formatter Pipeline

┌──────────────────────────────────────────────────────────────┐
│  FROM LINT RESULTS TO OUTPUT                                  │
│                                                              │
│  ESLint core                                                 │
│         │                                                    │
│         ▼                                                    │
│  ┌─────────────────────┐                                    │
│  │ LintResult[]        │  The results array                 │
│  │ (filePath, messages,│                                    │
│  │  counts, source)    │                                    │
│  └──────────┬──────────┘                                    │
│             │                                                │
│             ▼                                                    │
│  ┌─────────────────────┐                                    │
│  │ Formatter function  │  Receives results and context      │
│  │ (stylish, json, or  │                                    │
│  │  custom)            │                                    │
│  └──────────┬──────────┘                                    │
│             │                                                │
│             ▼                                                    │
│  ┌─────────────────────┐                                    │
│  │ String output       │  Printed to stdout                 │
│  └─────────────────────┘                                    │
└──────────────────────────────────────────────────────────────┘

The Two Built-In Defaults

┌──────────────────────────────────────────────────────────────┐
│  STYLISH VS JSON                                              │
│                                                              │
│  stylish                        json                         │
│  ┌─────────────────────┐       ┌─────────────────────┐     │
│  │ /path/app.js        │       │ [                   │     │
│  │  2:10 error ...     │       │   {                 │     │
│  │  3:20 warning ...   │       │     "filePath": ...,│     │
│  │                     │       │     "messages": [...│     │
│  │ ✖ 2 problems        │       │     "errorCount": 1,│     │
│  │                     │       │     ...             │     │
│  └─────────────────────┘       │   }                 │     │
│                                 │ ]                   │     │
│  Human-readable                 └─────────────────────┘     │
│  Colorized                      Machine-readable            │
│  Not parseable                  Parseable                   │
└──────────────────────────────────────────────────────────────┘

The Custom Formatter Function

┌──────────────────────────────────────────────────────────────┐
│  THE ENTIRE CONTRACT                                          │
│                                                              │
│  module.exports = function (results, context) {              │
│    // results: LintResult[]                                  │
│    // context: { cwd, maxWarningsExceeded, rulesMeta }       │
│                                                              │
│    return "formatted string";                                │
│  };                                                          │
│                                                              │
│  No configuration.                                           │
│  No plugin API.                                              │
│  Just a function that returns a string.                      │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Flag--format or -f
Default formatterstylish
Machine-readable formatterjson
Local formatter pathMust begin with .
npm formatter conventioneslint-formatter-*
Formatter signaturefunction (results, context) => string
Async supportESLint v8.4.0+
context fieldscwd, maxWarningsExceeded, rulesMeta

Key takeaways:

  • The formatter is a function, not a fixed output. It receives the results array and the context object and returns a string. The built-in json formatter is literally JSON.stringify(results, null, 2). A custom formatter is the same function with a different string .
  • stylish is for humans; json is for machines. The stylish formatter groups by file and colorizes the output for readability. The json formatter preserves the full structure, including fields that stylish omits, such as the ruleId, the fix object, and the source code .
  • The context argument provides run metadata. The cwd is the working directory, the maxWarningsExceeded indicates whether the warning limit was exceeded, and the rulesMeta contains the metadata for each rule that produced a message .
  • A local formatter path must begin with a period. The --format ./my-formatter.js syntax tells ESLint to resolve the file path. Without the period, the value is treated as the name of an npm package with the eslint-formatter-* prefix .
  • A formatter can be distributed as an npm package. The package name must follow the eslint-formatter-* convention, and the --format flag uses the short name. The package’s main entry must point to the formatter function .

Remember: ESLint’s output is a function call, not a fixed template. The stylish formatter is the default because it is the most readable for interactive development. The json formatter is the bridge to every tool that needs structured data. A custom formatter is the extension point for any format that the built-ins do not provide. The function signature is the entire API: results and context in, a string out. The --format flag selects the function. The rest is the function’s body.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!