Skip to main content

Custom Formatters

Custom formatters allow you to control how ESLint displays linting results. Whether you need JSON for CI/CD integration, HTML reports for dashboards, or custom formats for specific tools, formatters make it possible.

When to Create a Formatter

Create a custom formatter when you need to:
  • Generate reports for CI/CD systems
  • Create HTML dashboards or visualizations
  • Integrate with IDEs or editors
  • Export to specific file formats (XML, CSV, JUnit)
  • Apply custom styling or branding
  • Aggregate results across multiple runs
Built-in Formatters:ESLint includes several built-in formatters: stylish, compact, json, junit, html, and more. Use these as references for creating your own.

Formatter Structure

A formatter is a function that receives results and returns a string:
module.exports = function(results, context) {
  // Process results
  return "formatted output string";
};

Synchronous Formatter

my-formatter.js
module.exports = function(results, context) {
  let output = "";
  
  results.forEach(result => {
    output += `${result.filePath}\n`;
    
    result.messages.forEach(msg => {
      output += `  ${msg.line}:${msg.column} ${msg.message}\n`;
    });
  });
  
  return output;
};

Async Formatter

my-async-formatter.js
module.exports = async function(results, context) {
  // Perform async operations
  await sendToAPI(results);
  
  // Format and return
  return JSON.stringify(results, null, 2);
};

The Results Parameter

The results parameter is an array of LintResult objects:
[
  {
    filePath: "/path/to/file.js",
    messages: [
      {
        ruleId: "no-unused-vars",
        severity: 2,
        message: "'x' is defined but never used.",
        line: 2,
        column: 7,
        endLine: 2,
        endColumn: 8,
        nodeType: "Identifier"
      }
    ],
    errorCount: 1,
    warningCount: 0,
    fixableErrorCount: 0,
    fixableWarningCount: 0,
    source: "const x = 1;\nconsole.log('hello');\n"
  }
]

LintResult Properties

filePath
string
Absolute path to the linted file
messages
LintMessage[]
Array of lint messages for this file
errorCount
number
Number of errors (severity 2)
warningCount
number
Number of warnings (severity 1)
fixableErrorCount
number
Number of errors that can be auto-fixed
fixableWarningCount
number
Number of warnings that can be auto-fixed
source
string
Source code of the file (may be omitted)
suppressedMessages
LintMessage[]
Messages that were suppressed by inline comments

LintMessage Properties

ruleId
string | null
ID of the rule that generated the message
severity
0 | 1 | 2
Message severity: 0 (off), 1 (warn), 2 (error)
message
string
Human-readable error message
line
number
1-based line number where the issue occurs
column
number
1-based column number where the issue occurs
endLine
number
1-based line number where the issue ends
endColumn
number
1-based column number where the issue ends
fix
object
Auto-fix information (if available)
{
  range: [0, 5],
  text: "const"
}
suggestions
array
Alternative fixes that require manual approval

The Context Parameter

The context object provides additional information:
{
  cwd: "/project/root",
  color: true,  // or false, or undefined
  maxWarningsExceeded: {
    maxWarnings: 5,
    foundWarnings: 6
  },
  rulesMeta: {
    "no-unused-vars": {
      type: "problem",
      docs: {
        description: "Disallow unused variables",
        recommended: true,
        url: "https://eslint.org/docs/rules/no-unused-vars"
      },
      fixable: "code",
      messages: {
        unusedVar: "'{{varName}}' is defined but never used."
      }
    }
  }
}
cwd
string
Current working directory
color
boolean | undefined
Whether color output was requested (--color or --no-color)
maxWarningsExceeded
object | undefined
Present if --max-warnings was exceeded
  • maxWarnings: The limit that was set
  • foundWarnings: Actual number of warnings
rulesMeta
object
Metadata for all rules that were run

Example: Summary Formatter

Display only error and warning counts:
module.exports = function(results, context) {
  // Calculate totals
  const summary = results.reduce(
    (acc, result) => ({
      errors: acc.errors + result.errorCount,
      warnings: acc.warnings + result.warningCount
    }),
    { errors: 0, warnings: 0 }
  );
  
  // Format output
  if (summary.errors > 0 || summary.warnings > 0) {
    return `Errors: ${summary.errors}, Warnings: ${summary.warnings}\n`;
  }
  
  return "No problems found!\n";
};
Usage:
eslint -f ./my-formatter.js src/
Output:
Errors: 2, Warnings: 4

Example: Detailed Formatter

Include file paths and rule information:
module.exports = function(results, context) {
  const output = [];
  
  results.forEach(result => {
    if (result.messages.length === 0) return;
    
    output.push(`\nFile: ${result.filePath}`);
    
    result.messages.forEach(msg => {
      const severity = msg.severity === 2 ? "error" : "warning";
      const ruleInfo = context.rulesMeta[msg.ruleId];
      const ruleUrl = ruleInfo?.docs?.url || "";
      
      output.push(
        `  ${msg.line}:${msg.column} ${severity} ${msg.message} (${msg.ruleId})${
          ruleUrl ? ` ${ruleUrl}` : ""
        }`
      );
    });
  });
  
  return output.join("\n") + "\n";
};
Output:
File: /project/src/app.js
  2:7 error 'x' is defined but never used. (no-unused-vars) https://eslint.org/docs/rules/no-unused-vars
  5:1 warning Unexpected console statement. (no-console) https://eslint.org/docs/rules/no-console

Example: JSON Formatter

Format as JSON for tool integration:
module.exports = function(results, context) {
  // ESLint's built-in JSON formatter
  return JSON.stringify(results, null, 2);
};

Example: HTML Report

Generate an HTML dashboard:
module.exports = function(results, context) {
  const totalErrors = results.reduce((sum, r) => sum + r.errorCount, 0);
  const totalWarnings = results.reduce((sum, r) => sum + r.warningCount, 0);
  
  const html = `
<!DOCTYPE html>
<html>
<head>
  <title>ESLint Report</title>
  <style>
    body { font-family: Arial, sans-serif; margin: 20px; }
    .summary { padding: 20px; background: #f5f5f5; }
    .error { color: #d32f2f; }
    .warning { color: #f57c00; }
    .file { margin: 20px 0; border: 1px solid #ddd; padding: 15px; }
    .message { margin: 10px 0; padding: 10px; background: #fff; }
  </style>
</head>
<body>
  <h1>ESLint Report</h1>
  
  <div class="summary">
    <h2>Summary</h2>
    <p class="error">Errors: ${totalErrors}</p>
    <p class="warning">Warnings: ${totalWarnings}</p>
  </div>
  
  ${results.map(result => `
    <div class="file">
      <h3>${result.filePath}</h3>
      ${result.messages.map(msg => `
        <div class="message ${msg.severity === 2 ? 'error' : 'warning'}">
          <strong>Line ${msg.line}:${msg.column}</strong> - ${msg.message}
          <br><small>Rule: ${msg.ruleId}</small>
        </div>
      `).join('')}
    </div>
  `).join('')}
</body>
</html>
  `;
  
  return html;
};

Example: JUnit XML

Generate JUnit-compatible XML for CI systems:
function xmlEscape(str) {
  return str
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&apos;");
}

module.exports = function(results, context) {
  let output = '<?xml version="1.0" encoding="UTF-8"?>\n';
  output += '<testsuites>\n';
  
  results.forEach(result => {
    const testCount = result.messages.length;
    const failures = result.messages.filter(m => m.severity === 2).length;
    
    output += `  <testsuite name="${xmlEscape(result.filePath)}" tests="${testCount}" failures="${failures}">\n`;
    
    result.messages.forEach((msg, index) => {
      const testName = `${msg.ruleId}:${msg.line}:${msg.column}`;
      
      output += `    <testcase name="${xmlEscape(testName)}" classname="${xmlEscape(result.filePath)}">\n`;
      
      if (msg.severity === 2) {
        output += `      <failure message="${xmlEscape(msg.message)}">`;
        output += xmlEscape(`${msg.line}:${msg.column} ${msg.message}`);
        output += `</failure>\n`;
      }
      
      output += `    </testcase>\n`;
    });
    
    output += `  </testsuite>\n`;
  });
  
  output += '</testsuites>\n';
  
  return output;
};

Using Environment Variables

Customize formatter behavior with environment variables:
module.exports = function(results, context) {
  const skipWarnings = process.env.FORMATTER_SKIP_WARNINGS === "true";
  const verbose = process.env.FORMATTER_VERBOSE === "true";
  
  const summary = results.reduce(
    (acc, result) => {
      result.messages.forEach(msg => {
        const logMessage = {
          filePath: result.filePath,
          ruleId: msg.ruleId,
          message: msg.message,
          line: msg.line,
          column: msg.column
        };
        
        if (msg.severity === 1) {
          if (!skipWarnings) {
            acc.warnings.push(logMessage);
          }
        } else if (msg.severity === 2) {
          acc.errors.push(logMessage);
        }
      });
      return acc;
    },
    { errors: [], warnings: [] }
  );
  
  let output = "";
  
  [...summary.errors, ...summary.warnings].forEach(msg => {
    output += `${msg.filePath}:${msg.line}:${msg.column} ${msg.message}`;
    
    if (verbose) {
      output += ` (${msg.ruleId})`;
    }
    
    output += "\n";
  });
  
  return output;
};
Usage:
FORMATTER_SKIP_WARNINGS=true eslint -f ./my-formatter.js src/
FORMATTER_VERBOSE=true eslint -f ./my-formatter.js src/

Terminal-Friendly Output

Modern terminals support clickable links:
module.exports = function(results, context) {
  const output = [];
  
  results.forEach(result => {
    result.messages.forEach(msg => {
      // Format: file:line:column
      output.push(
        `${result.filePath}:${msg.line}:${msg.column} ` +
        `${msg.message} (${msg.ruleId})`
      );
    });
  });
  
  return output.join("\n") + "\n";
};
Output (clickable in many terminals):
/project/src/app.js:5:1 Unexpected console statement. (no-console)
/project/src/app.js:10:7 'x' is defined but never used. (no-unused-vars)

Color Support

Add colors when requested:
module.exports = function(results, context) {
  const useColor = context.color;
  
  const red = useColor ? "\x1b[31m" : "";
  const yellow = useColor ? "\x1b[33m" : "";
  const reset = useColor ? "\x1b[0m" : "";
  const bold = useColor ? "\x1b[1m" : "";
  
  const output = [];
  
  results.forEach(result => {
    if (result.messages.length === 0) return;
    
    output.push(`\n${bold}${result.filePath}${reset}`);
    
    result.messages.forEach(msg => {
      const color = msg.severity === 2 ? red : yellow;
      const level = msg.severity === 2 ? "error" : "warning";
      
      output.push(
        `  ${msg.line}:${msg.column} ` +
        `${color}${level}${reset} ` +
        `${msg.message} ` +
        `${color}(${msg.ruleId})${reset}`
      );
    });
  });
  
  return output.join("\n") + "\n";
};

Creating Your Formatter

1

Create Formatter File

my-formatter.js
module.exports = function(results, context) {
  // Start with a simple implementation
  const output = [];
  
  results.forEach(result => {
    output.push(result.filePath);
    
    result.messages.forEach(msg => {
      output.push(`  ${msg.line}:${msg.column} ${msg.message}`);
    });
  });
  
  return output.join("\n");
};
2

Test Locally

eslint -f ./my-formatter.js src/
3

Add Features

Enhance with:
  • Summary statistics
  • Color support
  • Rule metadata
  • Links to documentation
4

Package for Distribution

Create an npm package (optional):
package.json
{
  "name": "eslint-formatter-custom",
  "main": "index.js",
  "keywords": ["eslint", "eslint-formatter", "eslintformatter"]
}

Using a Custom Formatter

Local File

# Relative path (must start with .)
eslint -f ./formatters/my-formatter.js src/

# Absolute path
eslint -f /path/to/my-formatter.js src/

npm Package

# Install
npm install --save-dev eslint-formatter-custom

# Use (ESLint looks for eslint-formatter-* packages)
eslint -f custom src/

Packaging a Formatter

1

Create Package Structure

eslint-formatter-custom/
├── package.json
├── index.js
├── README.md
└── test/
    └── formatter.test.js
2

Configure package.json

{
  "name": "eslint-formatter-custom",
  "version": "1.0.0",
  "description": "Custom ESLint formatter",
  "main": "index.js",
  "keywords": [
    "eslint",
    "eslint-formatter",
    "eslintformatter"
  ],
  "peerDependencies": {
    "eslint": ">=9.0.0"
  }
}
3

Implement Formatter

index.js
module.exports = function(results, context) {
  // Your formatter implementation
};
4

Publish

npm publish

Testing Formatters

const assert = require("assert");
const formatter = require("./my-formatter");

const results = [
  {
    filePath: "/path/to/file.js",
    messages: [
      {
        ruleId: "no-unused-vars",
        severity: 2,
        message: "'x' is defined but never used.",
        line: 2,
        column: 7
      }
    ],
    errorCount: 1,
    warningCount: 0,
    fixableErrorCount: 0,
    fixableWarningCount: 0
  }
];

const context = {
  cwd: "/project",
  rulesMeta: {
    "no-unused-vars": {
      docs: {
        description: "Disallow unused variables"
      }
    }
  }
};

const output = formatter(results, context);

assert(output.includes("file.js"));
assert(output.includes("no-unused-vars"));
console.log("Tests passed!");

Advanced Patterns

Piping to External Tools

For complex formatting, use ESLint’s JSON output:
eslint -f json src/ | your-tool --process
Your tool receives:
const results = JSON.parse(stdin);
// Process and format as needed

Async Formatters

Perform asynchronous operations:
module.exports = async function(results, context) {
  // Upload to API
  await fetch("https://api.example.com/lint-results", {
    method: "POST",
    body: JSON.stringify(results)
  });
  
  // Return formatted output
  return `Results uploaded. Errors: ${totalErrors}\n`;
};

Stateful Formatters

Maintain state across runs (use with caution):
let runCount = 0;

module.exports = function(results, context) {
  runCount++;
  
  return `Run #${runCount}: ${totalErrors} errors\n`;
};

Best Practices

Follow these guidelines:
  1. Always return a string - Even if empty
  2. Handle empty results - Check for files with no messages
  3. Escape special characters - Especially in HTML/XML formatters
  4. Make output parseable - Use consistent formats
  5. Document your format - Explain the output structure
  6. Test with edge cases - No results, many errors, long paths

Error Handling

module.exports = function(results, context) {
  try {
    // Format logic
    return formatResults(results);
  } catch (error) {
    // Fallback to simple format
    console.error("Formatter error:", error);
    return JSON.stringify(results, null, 2);
  }
};

Real-World Examples

Learn from ESLint’s built-in formatters:

stylish

Default formatter with colors and summary

compact

One line per message for easy parsing

json

JSON output for tool integration

html

HTML report generator

junit

JUnit XML for CI systems

checkstyle

Checkstyle XML format

Community Formatters

Explore formatters on npm:
  • eslint-formatter-table - Table format
  • eslint-formatter-gitlab - GitLab code quality format
  • eslint-formatter-pretty - Enhanced stylish formatter
  • eslint-formatter-summary - Aggregated summary

Next Steps

Create a Plugin

Bundle formatter with rules and configs

Built-in Formatters

Explore all built-in formatters