| | |

ESLint 18 🛡️ –max-warnings — Enforcing warning thresholds in CI

⏱️ Estimated read: 13 min | |

ESLint’s default exit code behavior creates a blind spot in CI pipelines. Errors cause a non-zero exit code and fail the build, but warnings do not. A codebase can accumulate hundreds of warnings without the pipeline ever turning red, and the warnings become background noise that no one reads. The --max-warnings flag exists to close that gap. It sets a threshold: if the number of warnings exceeds the specified limit, ESLint exits with code 1, even though no errors were found . The flag turns warnings into a soft gate that can be tightened over time, from a high number that tolerates existing debt to zero that enforces a clean slate.

The flag is documented alongside the exit codes. When --max-warnings is set to n, exit code 0 means there are no errors and at most n warnings. Exit code 1 means there is at least one error, or there are more warnings than the limit allows . The behavior is precise: the count is the total number of warnings across all linted files, not per-file. A single file with one warning and another file with zero warnings produces a total of one, which is compared against the threshold.

Key point: --max-warnings converts the warning count into an exit code condition. A value of 0 means any warning fails the build. A higher value tolerates existing warnings while preventing new ones from accumulating. The flag is the mechanism for making warnings actionable in CI without converting every warning rule to an error.


Why warnings need a threshold

The invisible warning problem. A warning is, by ESLint’s design, a problem that does not affect the exit code. The documentation states this explicitly: setting a rule to "warn" means it “doesn’t affect exit code” . This is correct for local development, where a developer wants to see the warning without the process failing. But in CI, the exit code is the only signal that matters. A pipeline that runs eslint . and checks the exit code will pass regardless of how many warnings exist. The warnings scroll by in the log and are never addressed.

The incremental adoption problem. A team that wants to enforce a new rule cannot set it to "error" immediately if the existing codebase has hundreds of violations. The rule would fail every build on day one. Setting it to "warn" allows the violations to be reported without failing the build, but it also means the warnings are ignored. The --max-warnings flag provides the middle ground: the rule stays a warning, but the count of warnings is gated. The team can set the threshold to the current count, then ratchet it down as the violations are fixed. This is the same pattern as a budget that decreases over time .

The --quiet interaction. The --quiet flag suppresses warnings from the output. Before a fix in ESLint v7.17.0, --quiet also suppressed the --max-warnings failure, which meant a pipeline that used both flags would pass even when the warning count exceeded the limit. The behavior was corrected so that --max-warnings still produces exit code 1 even when --quiet hides the warnings . The two flags serve different purposes: --quiet hides the output, --max-warnings gates the count. They can be combined, but the gate remains active.

The trade-off. The flag requires a number. A team must decide what the threshold is, and that decision is not obvious. Setting it too high tolerates debt; setting it too low fails the build on day one. The recommended pattern is to start with the current warning count, commit that number, and reduce it with each cleanup effort. The number becomes a visible metric of the codebase’s health, and the CI failure when the number increases is the signal that a new warning was introduced.


a. The basic usage and the exit code behavior

The flag is passed on the command line with a numeric value. The value is the maximum number of warnings allowed before the build fails.

# Fail if there are more than zero warnings.
# This is the strictest setting: any warning fails the build.
npx eslint . --max-warnings 0
# Fail if there are more than 10 warnings.
# This tolerates existing debt while preventing new warnings
# from pushing the count above 10.
npx eslint . --max-warnings 10

The exit code behavior is documented in the CLI reference. Exit code 0 means no errors and the warning count is at most the limit. Exit code 1 means at least one error, or the warning count exceeds the limit . The distinction is important for a CI script that wants to handle the two cases differently. A script that checks $? can report “lint errors” for exit code 1 from errors, and “too many warnings” for exit code 1 from the threshold, but the exit code alone does not distinguish them. The output must be parsed to tell which condition caused the failure.

// package.json — a CI-oriented lint script
{
  "scripts": {
    "lint": "eslint .",
    "lint:ci": "eslint . --max-warnings 0"
  }
}

The two scripts serve different purposes. lint is for local development, where warnings are visible but not fatal. lint:ci is for the pipeline, where the warning count is gated. The lint:ci script can be combined with --quiet to suppress the warning output while keeping the gate active.

# Suppress warning output but keep the gate.
# The exit code is still 1 if the count exceeds the limit.
npx eslint . --quiet --max-warnings 0

The --quiet flag is documented as suppressing warnings, and the --max-warnings flag is documented as still failing when the count exceeds the limit, even with --quiet . The combination is the clean CI pattern: the log shows only errors, but the warning count is still enforced.


b. The incremental adoption pattern

The flag is most useful when the threshold is treated as a budget that decreases over time. The pattern requires a starting number, a ratchet mechanism, and a way to prevent the number from being ignored.

// package.json — the threshold as a script argument
{
  "scripts": {
    "lint:ci": "eslint . --max-warnings 42"
  }
}

The number 42 is the current warning count. The CI script fails if a new warning pushes the count to 43. The team fixes warnings in batches and lowers the number. The number is visible in the package.json, so it is reviewed in every pull request that changes it.

// A CI script that reads the count and compares it to a baseline.
// This is the pattern for a team that wants a more sophisticated ratchet.
import { ESLint } from "eslint";

const eslint = new ESLint();
const results = await eslint.lintFiles(["."]);

let warningCount = 0;
for (const result of results) {
  warningCount += result.warningCount;
}

// The baseline is committed to the repository.
// A new warning fails the build; a reduction is a successful cleanup.
const baseline = 42;

if (warningCount > baseline) {
  console.error(`Warning count increased: ${warningCount} > ${baseline}`);
  process.exitCode = 1;
} else if (warningCount < baseline) {
  console.log(`Warning count decreased: ${warningCount} < ${baseline}`);
  console.log("Update the baseline to lock in the improvement.");
}

The script-based approach gives more control than the CLI flag. It can distinguish between an increase and a decrease, and it can prompt the developer to update the baseline when the count improves. The CLI flag is simpler, but it treats any count above the threshold as a failure, regardless of whether the count decreased from a previous run. The script is the pattern for a team that wants the threshold to ratchet automatically.

The --max-warnings flag is also relevant to the --no-warn-ignored combination. When a file is explicitly passed on the command line but is ignored by the configuration, ESLint emits a warning. If --max-warnings 0 is used, that warning causes the build to fail. The --no-warn-ignored flag suppresses the warning, which prevents the failure . This is the fix for a CI script that passes a glob that matches ignored files.


Complete Example Session

// ============================================
// PART 1: THE DEFAULT BEHAVIOR
// ============================================

// A rule set to "warn" does not affect the exit code.
// eslint . exits 0 even if there are 100 warnings.
// The warnings are reported but the build passes.
// This is the blind spot that --max-warnings addresses.

// ============================================
// PART 2: THE BASIC FLAG
// ============================================

// npx eslint . --max-warnings 0
// Exit code 1 if any warning exists.
// Exit code 0 if there are no warnings and no errors.
// This is the strictest setting.

// ============================================
// PART 3: THE THRESHOLD
// ============================================

// npx eslint . --max-warnings 10
// Exit code 1 if the total warning count is 11 or more.
// Exit code 0 if the count is 10 or fewer.
// The count is across all files, not per-file.

// ============================================
// PART 4: THE EXIT CODE DOCUMENTATION
// ============================================

// From the ESLint CLI reference:
// 0: no linting errors, and warning count ≤ n (if --max-warnings n is set)
// 1: at least one linting error, or warning count > n
// 2: configuration problem or internal error

// ============================================
// PART 5: THE --quiet COMBINATION
// ============================================

// npx eslint . --quiet --max-warnings 0
// --quiet suppresses warning output.
// --max-warnings 0 still fails if any warning exists.
// The log shows only errors, but the gate is active.
// This is the clean CI pattern.

// ============================================
// PART 6: THE INCREMENTAL ADOPTION PATTERN
// ============================================

// Start with the current warning count.
// package.json: "lint:ci": "eslint . --max-warnings 42"
// The CI passes with 42 warnings.
// A new warning makes it 43, and the CI fails.
// Fix a batch of warnings, lower the number to 30.
// Repeat until the number is 0.

// ============================================
// PART 7: THE SCRIPT-BASED RATCHET
// ============================================

// A more sophisticated pattern reads the count
// and compares it to a committed baseline.
// See the script in the section above.
// The script distinguishes an increase from a decrease.
// An increase fails the build.
// A decrease prompts the developer to update the baseline.

// ============================================
// PART 8: THE IGNORED FILE WARNING
// ============================================

// Passing an ignored file explicitly produces a warning.
// With --max-warnings 0, that warning fails the build.
// The fix is --no-warn-ignored.
// npx eslint --no-warn-ignored --max-warnings 0 ignored-file.js
// This suppresses the ignore warning and keeps the gate active.

// ============================================
// PART 9: THE CI SCRIPT
// ============================================

// package.json
{
  "scripts": {
    "lint": "eslint .",
    "lint:ci": "eslint . --quiet --max-warnings 0"
  }
}

// The CI runs `npm run lint:ci`.
// Errors fail the build (exit code 1).
// Warnings fail the build (exit code 1, via the flag).
// The output shows only errors (via --quiet).
// The pipeline is clean: it fails on both errors and warnings.

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

// [ ] The --max-warnings flag is passed a numeric value.
// [ ] The value is the current warning count or 0.
// [ ] The --quiet flag is used to suppress warning output in CI.
// [ ] The --no-warn-ignored flag is used if ignored files are passed.
// [ ] The threshold is lowered as warnings are fixed.
// [ ] The exit code is checked in the CI script.

The ten parts covered the default behavior, the basic flag, the threshold, the exit code documentation, the --quiet combination, the incremental adoption pattern, the script-based ratchet, the ignored file warning, the CI script, and the verification checklist. The through-line is that the flag converts the warning count into a gate, and the gate is only useful if the threshold is maintained.


Quick Reference

The exit codes

CodeMeaning
0No errors; warning count ≤ --max-warnings n
1At least one error, or warning count > n
2Configuration problem or internal error

The flag combinations

CombinationEffect
--max-warnings 0Any warning fails the build
--max-warnings 10More than 10 warnings fails the build
--quiet --max-warnings 0Warnings hidden but still gated
--no-warn-ignored --max-warnings 0Ignored-file warnings suppressed, gate active

The incremental pattern

StepAction
1Set the threshold to the current warning count
2Fix warnings in batches
3Lower the threshold after each batch
4Repeat until the threshold is 0

Best Practices

✅ Do This:

# Use --max-warnings in CI
npx eslint . --max-warnings 0                                    # ✅

# Combine with --quiet for a clean log
npx eslint . --quiet --max-warnings 0                            # ✅

# Lower the threshold as warnings are fixed
"lint:ci": "eslint . --max-warnings 10" // then 5, then 0       # ✅

# Use --no-warn-ignored if passing ignored files
npx eslint --no-warn-ignored --max-warnings 0 file.js           # ✅

❌ Don’t Do This:

# Rely on the default exit code for warnings
npx eslint . # warnings do not affect exit code                   # ❌

# Set the threshold to a number and never lower it
"lint:ci": "eslint . --max-warnings 100" // debt tolerated        # ❌

# Expect --quiet to suppress the max-warnings failure
npx eslint . --quiet --max-warnings 0 # still exits 1            # ❌

Common Pitfalls

PitfallWhy It HappensFix
CI passes despite warnings--max-warnings not usedAdd the flag
CI fails on ignored files--max-warnings 0 and ignored-file warningAdd --no-warn-ignored
Threshold is never loweredThe number is set and forgottenTreat it as a budget and ratchet
--quiet hides the failure reasonThe warning output is suppressedCheck the exit code, not just the log
The count is per-file, not totalThe flag counts total warningsVerify the total, not individual files

Real-World Examples

1. Strict CI gate

npx eslint . --max-warnings 0

2. Tolerant threshold

npx eslint . --max-warnings 10

3. Quiet CI

npx eslint . --quiet --max-warnings 0

4. Package script

"lint:ci": "eslint . --max-warnings 0"

5. Ignored file handling

npx eslint --no-warn-ignored --max-warnings 0 file.js

6. Incremental adoption

"lint:ci": "eslint . --max-warnings 42"

7. Script-based ratchet

if (warningCount > baseline) process.exitCode = 1;

8. Decrease detection

if (warningCount < baseline) console.log("Update the baseline");

9. Exit code check

npx eslint . --max-warnings 0; echo $?

10. Default vs gated

npx eslint .          # exit 0 with warnings
npx eslint . --max-warnings 0  # exit 1 with warnings

Visual

The Warning Gate

┌──────────────────────────────────────────────────────────────┐
│  WITHOUT AND WITH --max-warnings                              │
│                                                              │
│  Without                        With --max-warnings 0        │
│  ┌─────────────────────┐       ┌─────────────────────┐     │
│  │ 50 warnings         │       │ 50 warnings         │     │
│  │ 0 errors            │       │ 0 errors            │     │
│  │                     │       │                     │     │
│  │ Exit code: 0        │       │ Exit code: 1        │     │
│  │ CI passes           │       │ CI fails            │     │
│  └─────────────────────┘       └─────────────────────┘     │
│                                                              │
│  The warnings are reported in both cases.                   │
│  The flag changes the exit code, not the report.            │
└──────────────────────────────────────────────────────────────┘

The Incremental Adoption

┌──────────────────────────────────────────────────────────────┐
│  LOWERING THE THRESHOLD OVER TIME                             │
│                                                              │
│  Week 1: --max-warnings 100                                  │
│  ┌─────────────────────┐                                    │
│  │ 100 warnings        │  CI passes                         │
│  │ 0 errors            │                                    │
│  └─────────────────────┘                                    │
│                                                              │
│  Week 2: --max-warnings 60                                   │
│  ┌─────────────────────┐                                    │
│  │ 60 warnings         │  CI passes                         │
│  │ 0 errors            │                                    │
│  └─────────────────────┘                                    │
│                                                              │
│  Week 3: --max-warnings 0                                    │
│  ┌─────────────────────┐                                    │
│  │ 0 warnings          │  CI passes                         │
│  │ 0 errors            │                                    │
│  └─────────────────────┘                                    │
│                                                              │
│  Each reduction is a cleanup milestone.                      │
│  The threshold is the visible metric of the debt.            │
└──────────────────────────────────────────────────────────────┘

The –quiet Interaction

┌──────────────────────────────────────────────────────────────┐
│  OUTPUT VS EXIT CODE                                          │
│                                                              │
│  --quiet --max-warnings 0                                    │
│  ┌─────────────────────┐                                    │
│  │ Log output:         │  Errors only                       │
│  │ (warnings hidden)   │                                    │
│  └──────────┬──────────┘                                    │
│             │                                                │
│             ▼                                                    │
│  ┌─────────────────────┐                                    │
│  │ Exit code: 1        │  If any warning exists             │
│  │ (gate still active) │                                    │
│  └─────────────────────┘                                    │
│                                                              │
│  The flag hides the output but does not disable the gate.   │
│  The pipeline still fails on the warning count.             │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Flag--max-warnings <n>
Exit code 1 conditionErrors exist, or warning count > n
Strict setting--max-warnings 0
Output suppression--quiet (gate remains active)
Ignored file fix--no-warn-ignored
Adoption patternStart with current count, lower over time

Key takeaways:

  • Warnings do not affect the exit code by default. A rule set to "warn" reports the problem but does not fail the build. The --max-warnings flag is the mechanism that turns the warning count into a gate .
  • The flag sets a total count threshold, not a per-file threshold. Exit code 1 occurs when the total number of warnings across all linted files exceeds the specified value. Exit code 0 means the count is at or below the threshold and there are no errors .
  • --quiet does not disable the gate. The flag suppresses warning output from the log, but the --max-warnings failure still occurs. The two flags can be combined for a clean CI log with an active gate .
  • The threshold is most useful as a budget that decreases. Start with the current warning count, commit that number, and lower it as warnings are fixed. The CI failure when the count increases is the signal that a new warning was introduced. The number is a visible metric of the codebase’s debt .
  • --no-warn-ignored prevents a false failure. When an ignored file is passed explicitly, ESLint emits a warning. With --max-warnings 0, that warning fails the build. The --no-warn-ignored flag suppresses the warning without disabling the gate .

Remember: --max-warnings is the flag that makes warnings matter in CI. Without it, a codebase can accumulate warnings indefinitely because the exit code never changes. With it, the warning count becomes a number that the pipeline enforces. The number can be zero for a clean codebase, or a decreasing budget for one that is cleaning up existing debt. The flag does not change what ESLint reports; it changes whether the report fails the build. The gate is the flag, and the budget is the number.

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!