Skip to main content

Node

Node Plugin Configuration

To enhance code quality, maintainability, and enforce best practices in your Node projects, the Eslint Plugin Hub provides several Node.js-focused rules. These rules help manage code complexity and promote efficient memory usage patterns critical for server-side applications.

Node Rules

Rule NameDescription
minimize-complex-flowsEnforces simplified control flow by limiting recursion and nesting depth, and detecting direct or lexically scoped recursion to improve readability and reduce error potential.
avoid-runtime-heap-allocationDiscourages heap allocation of common data structures (arrays, objects, Maps, Sets) within function bodies, especially in loops, to promote reuse and reduce GC pressure.
limit-reference-depthRestricts the depth of chained property access and enforces optional chaining to prevent runtime errors, improve null safety, and encourage safer access patterns in deeply nested data structures.
keep-functions-conciseEnforces a maximum number of lines per function, with options to skip blank lines and comments, to promote readability, maintainability, and concise logic blocks.
fixed-loop-boundsEnforces that loops have clearly defined termination conditions to prevent infinite loops.
limit-data-scopeEnforces several best practices for data scoping: disallows global object modification, suggests moving variables to their narrowest functional scope, and discourages var usage.
use-runtime-assertionsEnforces the presence of a minimum number of runtime assertions in functions to validate inputs and critical intermediate values, promoting early error detection and contract-based programming.
minimize-deep-asynchronous-chainsLimits the depth of Promise chains (.then/.catch/.finally) and the number of await expressions within async functions to improve readability and manage complexity in asynchronous operations.
check-return-valuesEnforces handling of return values from non-void functions. Ignored values should be explicitly marked via void, underscore assignment, or a specific comment.

Configuration

After installing the plugin (npm install @mindfiredigital/eslint-plugin-hub --save-dev), you'll need to add the Node.js-specific rules or configurations from @mindfiredigital/eslint-plugin-hub to your ESLint configuration file (e.g., eslintrc.config.js,.eslintrc.json, .eslintrc.js, or .eslintrc.yaml).

ESLint v9+ uses eslint.config.js (flat config). Older versions use .eslintrc.js (or .json, .yaml).

For Flat Config (e.g., eslint.config.js):

You can extend a pre-defined Node.js configuration from the plugin or configure rules manually.

// eslint.config.js
import hub from '@mindfiredigital/eslint-plugin-hub';

export default [
{
files: ['**/*.js', '**/*.ts'], // Or more specific files like 'src/**/*.ts'
plugins: {
hub: hub, // 'hub' is the prefix for your rules
},
languageOptions: {
globals: {
...globals.node, // Recommended for Node.js projects
},
parserOptions: {
ecmaVersion: 2022, // Or your target ECMAScript version
sourceType: 'module',
},
},
rules: {
'hub/minimize-complexflows': [
'warn',
{
/* options */
},
],
'hub/avoid-runtime-heap-allocation': [
'warn',
{
/* options */
},
],
'hub/fixed-loop-bounds': [
'warn',
{
/* options */
},
],
'hub/limit-data-scope': [
'warn',
{
/* options */
},
],
'hub/limit-reference-depth': [
'warn',
{
/* options */
},
],
'hub/keep-functions-concise': [
'warn',
{
/* options */
},
],
'hub/use-runtime-assertions': [
'warn',
{
minAssertions: 2,
assertionUtilityNames: ['assert', 'invariant', 'check'],
ignoreEmptyFunctions: true,
},
],
'hub/minimize-deep-asynchronous-chains': [
'warn',
{
maxPromiseChainLength: 3,
maxAwaitExpressions: 3,
},
],
'hub/check-return-values': ['warn'],
// ... any additional rule overrides or additions
},
},
];

Node.js Rule Details

1. minimize-complexflows

Description: Excessive complexity in how a program flows from one instruction to another significantly increases the risk of introducing logic errors that can be hard to find. When code paths become convoluted due to deeply nested structures (like multiple if statements inside each other) or complicated recursive calls, the code becomes much more difficult for developers to read, understand, and mentally trace.

Rationale: This difficulty directly impacts maintainability; making changes or adding new features to overly complex code is a challenging and error-prone task. Furthermore, testing all possible paths in such code becomes exponentially harder, leading to less reliable software.

By promoting simpler, more linear, or well-structured control flows, this rule aims to make your code easier to verify, debug, and test. Code that is straightforward in its execution path is generally more robust and less prone to hidden bugs, leading to higher overall software quality and a more efficient development process. Limiting recursion to scenarios with clear, bounded termination conditions also helps prevent stack overflows and makes the recursive logic easier to reason about.

Options: The rule accepts a single object with the following properties:

maxNestingDepth

Type: number Description: Specifies the maximum allowed depth of nested control structures (like if, for, while, switch). Nesting beyond this depth will be flagged. Default: 3 Constraint: Must be a minimum of 1. Example Usage: JavaScript

{
rules: { "hub/minimize-complexflows": ["warn", { "maxNestingDepth": 4 }]
}
}

This would allow nesting up to 4 levels deep.

allowRecursion

Type: boolean Description: Determines whether recursive function calls (both direct and lexical) are permitted. If set to false, the rule will flag instances of recursion. If true, recursion checks are disabled. Default: false (meaning recursion is flagged by default) Example Usage: JavaScript

{
rules: { "hub/minimize-complexflows": ["warn", { "allowRecursion": true }]
}
}
This would allow recursive functions without ESLint warnings from this rule.
Example of Full Configuration in eslint.config.js:

JavaScript

// eslint.config.js
// ... other imports and configurations ...
{
plugins: {
"hub": hub,
},
rules: {
"hub/minimize-complexflows": ["warn", {
"maxNestingDepth": 2, // Stricter nesting
"allowRecursion": true // Allow recursion
}],
// ... other rules
}
}
// ...

These two options (maxNestingDepth and allowRecursion) give you control over how strictly the minimize-complex-flows rule operates in your project.

Example:

"hub/minimize-complexflows": [{ "maxNestingDepth": 3, "allowRecursion": false }]

Valid: Nesting up to 3 levels

function processOrder(order) {
if (order) {
// Level 1
if (order.items && order.items.length > 0) {
// Level 2
for (const item of order.items) {
// Level 3
console.log(item.name);
}
}
}
}

Valid: No recursion

function calculateSum(numbers) {
let sum = 0;
for (const num of numbers) {
sum += num;
}
return sum;
}

Invalid: Nesting depth of 4 (exceeds maxNestingDepth: 3)

function checkPermissions(user, resource, action) {
if (user) {
// Level 1
if (user.roles) {
// Level 2
if (user.roles.includes('admin')) {
// Level 3
if (resource.isProtected && action === 'delete') {
// Level 4 - ERROR
console.log('Admin delete allowed');
return true;
}
}
}
}
return false;
}

ESLint Warning: Avoid nesting control structures deeper than 3 levels. Current depth: 4.

Invalid: Direct recursion (allowRecursion: false)

function countdown(n) {
if (n <= 0) {
console.log('Blast off!');
return;
}
console.log(n);
countdown(n - 1); // ERROR: Direct recursion
}

ESLint Warning: Direct recursion detected in function countdown.

Invalid: Lexical recursion (allowRecursion: false)

function outerTask(value) {
console.log('Outer task:', value);
function innerTask(innerValue) {
if (innerValue > 0) {
console.log('Inner task, calling outer:', innerValue);
outerTask(innerValue - 1); // ERROR: Lexical recursion
}
}
if (value > 0) {
innerTask(value);
}
}

ESLint Warning: Lexical recursion: function outerTask is called from an inner scope of innerTask

"hub/minimize-complexflows": ["warn", { "maxNestingDepth": 2, "allowRecursion": false }]

Valid: Nesting up to 2 levels

function checkAccess(user) {
if (user) {
// Level 1
if (user.isActive) {
// Level 2
return true;
}
}
return false;
}

Invalid: Nesting depth of 3 (exceeds maxNestingDepth: 2)

function processOrder(order) {
if (order) {
// Level 1
if (order.items && order.items.length > 0) {
// Level 2
for (const item of order.items) {
// Level 3 - ERROR
console.log(item.name);
}
}
}
}

ESLint Warning: Avoid nesting control structures deeper than 2 levels. Current depth: 3.

"hub/minimize-complex-flows": ["warn", { "maxNestingDepth": 3, "allowRecursion": true }]

Valid: Direct recursion is now allowed

function factorial(n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1); // OK, recursion allowed
}

Valid: Lexical recursion is now allowed

function outerWithAllowedRecursion(value) {
function innerCallOuter(innerValue) {
if (innerValue > 0) {
outerWithAllowedRecursion(innerValue - 1); // OK, recursion allowed
}
}
if (value > 0) {
innerCallOuter(value);
}
}

Valid: Nesting still respects maxNestingDepth

function normalNestingWithRecursionAllowed(data) {
if (data) {
// Level 1
if (data.value > 0) {
// Level 2
console.log(data.value);
}
}
}

Invalid: Nesting depth of 4 (exceeds maxNestingDepth: 3), even if recursion is allowed

function deeplyNestedButRecursionAllowed(user) {
if (user) {
// Level 1
if (user.profile) {
// Level 2
if (user.profile.settings) {
// Level 3
if (user.profile.settings.isActive) {
// Level 4 - ERROR
console.log('User is active');
return true;
}
}
}
}
return false;
}

ESLint Warning: Avoid nesting control structures deeper than 3 levels. Current depth: 4.

2. avoid-runtime-heap-allocation

Description: Encourages efficient memory management by discouraging the creation of new common data structures (arrays [], objects , new Map(), new Set(), etc.) directly within function bodies, and especially inside loops. This practice helps to reduce garbage collection pressure and improve performance.

Okay, I understand! You want the documentation for avoid-runtime-heap-allocation formatted precisely like the Docusaurus-style Markdown you provided for the minimize-complex-flows rule.

Here's the documentation for node/avoid-runtime-heap-allocation in that format:

node/avoid-runtime-heap-allocation Description: Encourages efficient memory management by discouraging the creation of new common data structures (arrays [], objects , new Map(), new Set(), etc.) directly within function bodies, and especially inside loops. This practice helps to reduce garbage collection pressure and improve performance.

Rationale: Frequent dynamic memory allocations and deallocations during an application's runtime can lead to several performance issues. These include general performance degradation due to the overhead of memory management, excessive garbage collection (GC) cycles which can pause application execution, and memory fragmentation. Over time, fragmentation can make it difficult for the system to find contiguous blocks of memory, even if sufficient total memory is free. Keeping memory usage predictable and minimizing runtime allocations are crucial for long-running, resource-intensive, or real-time applications, ensuring smoother operation and stability. This rule promotes pre-allocation and reuse of data structures where feasible.

Options: The rule accepts a single object with the following properties:

checkLoopsOnly

Type: boolean Description: If set to true, the rule will only flag allocations that occur inside loops within functions. If false (default), it flags any such allocation found anywhere inside a function body (outside of module scope). Default: false Example Usage: JSON

In your ESLint config rules section:

{
rules: {"hub/avoid-runtime-heap-allocation": ["warn", { "checkLoopsOnly": true }]
}
}

allowedConstructs

Type: array of string Description: A list of constructor names that should be exempt from this rule, allowing their allocation at runtime without warning. Default: [] (empty array, meaning all targeted constructs are checked by default) Enum Values: 'Array', 'Object', 'Map', 'Set', 'WeakMap', 'WeakSet' Example Usage: JSON

// In your ESLint config rules section:

{
rules: { "hub/avoid-runtime-heap-allocation": ["warn", { "allowedConstructs": ["Map", "Set"] }]
}
}

Example of Full Configuration in eslint.config.js:

// eslint.config.js
// Assuming 'hubPlugin' is your imported plugin '@mindfiredigital/eslint-plugin-hub'
// ... other imports and configurations ...
{
plugins: {
"hub": hubPlugin,
},
rules: {
"hub/avoid-runtime-heap-allocation": ["warn", {
"checkLoopsOnly": false, // Example: check everywhere in functions
"allowedConstructs": ["Set"] // Example: Allow 'new Set()'
}],
// ... other rules
}
}
// ...

These two options (checkLoopsOnly and allowedConstructs) give you control over how strictly the avoid-runtime-heap-allocation rule operates in your project.

Example:

"hub/avoid-runtime-heap-allocation": ["warn"] which implies { "checkLoopsOnly": false, "allowedConstructs": [] }

Valid (Should NOT produce warnings from this rule):

// Module-scope allocations are fine
const globalArray = [];
const globalObject = {};

function usesGlobalArray(data) {
globalArray.length = 0; // Modifying, not re-allocating
globalArray.push(...data);
}

// Empty array/object as default parameter (ignored by rule heuristic)
function processItems(items = []) {
console.log(items);
}

Invalid (Should PRODUCE warnings from this rule):

// Allocation in a function
function processData(data) {
const tempResults = []; // Invalid: allocationInFunction
data.forEach(item => tempResults.push(item * 2));
return tempResults;
}

ESLint Warning: Runtime allocation of 'Array' ([]) detected in function processData. Consider pre-allocating and reusing, especially if this function is called frequently or is performance-sensitive.

function createConfig() {
const config = { active: true, mode: 'test' }; // Invalid: allocationInFunction
return config;
}

ESLint Warning: Runtime allocation of 'Object' ({ active: true, mode: '...}) detected in function createConfig. Consider pre-allocating and reusing, especially if this function is called frequently or is performance-sensitive.

// Allocation in a loop within a function
function processBatch(batch) {
for (let i = 0; i < batch.length; i++) {
const itemSpecificData = [batch[i].id, batch[i].value]; // Invalid: allocationInLoop
}
}

ESLint Warning: Runtime allocation of 'Array' ([batch[i].id, batch[i]...]) detected inside a loop within function processBatch. This can severely impact performance. Pre-allocate and reuse this structure.

Scenario 2: Option checkLoopsOnly: true ("hub/avoid-runtime-heap-allocation": ["warn", { "checkLoopsOnly": true }])

Valid (Should NOT produce warnings from this rule):

// Allocation in a function, but NOT in a loop, is now VALID
function processData(data) {
const tempResults = []; // Valid with checkLoopsOnly: true
data.forEach(item => tempResults.push(item * 2));
return tempResults;
}
Invalid (Should PRODUCE warnings from this rule):
// Allocation in a loop within a function is still INVALID
function processBatchWithLoopCheck(batch) {
const initialItems = []; // This is VALID with checkLoopsOnly: true
for (let i = 0; i < batch.length; i++) {
const itemSpecificData = { id: batch[i].id }; // Invalid: allocationInLoop
}
}

ESLint Warning: Runtime allocation of 'Object' ({ id: batch[i].id }) detected inside a loop within function processBatchWithLoopCheck. This can severely impact performance. Pre-allocate and reuse this structure.

Scenario 3: Option allowedConstructs: ['Map'] ("hub/node/avoid-runtime-heap-allocation": ["warn", { "allowedConstructs": ["Map"] }])

Valid (Should NOT produce warnings from this rule for Map):

function useAllowedTypes() {
const myMap = new Map(); // Valid: Map is in allowedConstructs

for (let i = 0; i < 2; i++) {
const mapInLoop = new Map(); // Valid: Map is in allowedConstructs
}
}

Invalid (Should PRODUCE warnings from this rule for Array/Object):

function useMixedTypes() {
const myMap = new Map(); // Valid: Map is in allowedConstructs
const myArray = []; // Invalid: allocationInFunction for Array

for (let i = 0; i < 2; i++) {
const objInLoop = { index: i }; // Invalid: allocationInLoop for Object
}
}

ESLint Warning: Runtime allocation of 'Array' ([]) detected in function useMixedTypes. Consider pre-allocating and reusing, especially if this function is called frequently or is performance-sensitive. ESLint Warning: Runtime allocation of 'Object' ({ index: i }) detected inside a loop within function useMixedTypes. This can severely impact performance. Pre-allocate and reuse this structure.

3. fixed-loop-bounds

Description: This rule helps prevent infinite loops by ensuring that while, do...while, and for loops have clearly defined and reachable termination conditions. It specifically targets loops that use true as a condition or rely on external flags that are not modified within the loop body.

Rationale: Infinite loops can cause applications to hang, consume excessive resources, and are a common source of bugs. Statically analyzing loop conditions helps catch these potential issues early.

Options: The rule accepts an object with the following optional properties:

  • disallowInfiniteWhile (boolean, default: true): If true, flags while(true), do...while(true), for(;;), and for(;true;) loops that do not have an effective break statement within their body.
  • disallowExternalFlagLoops (boolean, default: true): If true, flags while or do...while loops whose condition is an identifier (or its negation like !flag) that is not reassigned or updated within the loop's body.

Important Implementation Details:

  • The rule performs static analysis to detect break statements that effectively terminate the loop
  • It handles labeled break statements correctly
  • It ignores breaks inside nested functions as they don't affect the outer loop
  • For external flag detection, it checks for assignment expressions (flag = value) and update expressions (flag++, ++flag, flag--, --flag)
  • Modifications inside nested functions are not considered as they operate in different scopes

Examples of Incorrect Code:

// Incorrect: while(true) without a break
while (true) {
console.log('potentially infinite');
// No break statement found
}

// Incorrect: for(;;) without a break
for (;;) {
// This loop will run forever
performTask();
}

// Incorrect: for loop with true condition but no break
for (; true; ) {
console.log('infinite loop');
}

// Incorrect: External flag not modified within the loop
let keepRunning = true;
while (keepRunning) {
// 'keepRunning' is never set to false inside this loop
performTask();
}

// Incorrect: Negated flag condition not modified
let shouldStop = false;
while (!shouldStop) {
// 'shouldStop' is never set to true inside this loop
doWork();
}

// Incorrect: do-while with true condition and no break
do {
processData();
} while (true); // No break statement in the body

Examples of Correct Code:

// Correct: while(true) with a break
while (true) {
if (conditionMet()) {
break;
}
console.log('looping');
}

// Correct: for loop with a clear condition
for (let i = 0; i < 10; i++) {
console.log(i);
}

// Correct: External flag modified within the loop (assignment)
let processNext = true;
while (processNext) {
if (!processItem()) {
processNext = false; // Flag is modified via assignment
}
}

// Correct: External flag modified within the loop (update expression)
let counter = 10;
while (counter) {
performTask();
counter--; // Flag is modified via update expression
}

// Correct: Labeled break statement
outerLoop: while (true) {
for (let i = 0; i < 5; i++) {
if (shouldExit()) {
break outerLoop; // Correctly targets the outer loop
}
}
}

// Correct: Break inside nested scope but not nested function
while (true) {
{
if (condition) {
break; // This break correctly exits the while loop
}
}
}

When Not To Use It: You might consider disabling disallowExternalFlagLoops if you have loops where the controlling flag is intentionally modified by asynchronous operations or in a deeply nested utility function whose side effects on the flag are not easily detectable by static analysis (though this is generally an anti-pattern for loop control).

4. limit-data-scope

Description: Enforces several best practices for data scoping to improve code maintainability and prevent common JavaScript pitfalls. This rule helps developers write cleaner, more organized code by discouraging global object modifications, encouraging proper variable scoping, and promoting modern variable declarations.

Rationale: Poor data scoping practices lead to hard-to-maintain code, namespace pollution, and subtle bugs. Global object modifications can cause conflicts between different parts of an application or third-party libraries. Variables declared in overly broad scopes create unnecessary coupling and make code harder to understand and refactor. The var keyword's function-scoping behavior is often counterintuitive and can lead to hoisting-related bugs.

This rule enforces three key practices:

  1. No Global Object Modification: Prevents direct modification of global objects like window, global, and globalThis
  2. Narrowest Scope: Suggests moving variables to their most restrictive scope when they're only used within a single function
  3. Modern Variable Declarations: Discourages var usage in favor of let and const

Examples

✅ Valid Code (Should NOT produce warnings)

No Global Object Modification

// Reading from global objects is allowed
const config = window.location || {};
console.log(global.process);
const val = globalThis.crypto;

// Modifying local objects is fine
const myVar = {};
myVar.prop = 1;

// Shadowed global variables are allowed
function foo() {
let window = {};
window.bar = 1; // This 'window' is local, not global
}

Proper Variable Scoping

// Variable used in multiple functions - correctly at module level
const sharedVar = 10;
function funcA() {
console.log(sharedVar);
}
function funcB() {
console.log(sharedVar);
}

// Variable used at module scope - correctly placed
const moduleVar = 20;
console.log(moduleVar); // Used at module scope
function useIt() {
console.log(moduleVar);
}

// Variable already in narrowest scope
function doSomething() {
const localVar = 30; // Already in narrowest scope
console.log(localVar);
}

Modern Variable Declarations

// Use let and const instead of var
let x = 1;
const y = 2;
for (let i = 0; i < 5; i++) {}

function test() {
const local = 1;
return local;
}

❌ Invalid Code (Should PRODUCE warnings)

Global Object Modification

// ESLint Error: Avoid modifying the global object "window". "myCustomProperty" should not be added globally.
window.myCustomProperty = 123;

// ESLint Error: Avoid modifying the global object "global". "debug" should not be added globally.
global.debug = true;

// ESLint Error: Avoid modifying the global object "globalThis". "newProperty" should not be added globally.
globalThis['newProperty'] = 'value';

// ESLint Error: Avoid modifying the global object "window". "customHandler" should not be added globally.
function setup() {
window.customHandler = function () {};
}

Variables in Overly Broad Scope

// ESLint Warning: Variable 'onlyInFuncA' is declared in module scope but appears to be used only within the 'funcA' function scope. Consider moving its declaration into the 'funcA' scope.
const onlyInFuncA = 100;
function funcA() {
console.log(onlyInFuncA); // Only used here
}
function funcB() {
/* does not use onlyInFuncA */
}

// ESLint Warning: Variable 'configValue' is declared in module scope but appears to be used only within the 'initialize' function scope. Consider moving its declaration into the 'initialize' scope.
let configValue;
function initialize() {
configValue = { setting: true };
console.log(configValue);
}

Using var Instead of let/const

// ESLint Warning: Prefer 'let' or 'const' over 'var' for variable 'z'.
var z = 3;

// ESLint Warning: Prefer 'let' or 'const' over 'var' for variable 'count'.
function oldStyle() {
var count = 0;
return count;
}

// ESLint Warning: Prefer 'let' or 'const' over 'var' for variable 'i'.
for (var i = 0; i < 10; i++) {
console.log(i);
}

Combined Violations

// Multiple violations in one code block
var utilityData = { helper: true }; // var usage violation
function doWork() {
window.workResult = utilityData.helper; // global modification + scope violation
}
// ESLint Warnings:
// 1. Prefer 'let' or 'const' over 'var' for variable 'utilityData'
// 2. Variable 'utilityData' should be moved to narrower scope
// 3. Avoid modifying the global object "window"

When to Disable

Consider disabling this rule in specific cases:

/* eslint-disable hub/limit-data-scope */
// Legitimate polyfill or library initialization
if (!window.customLibrary) {
window.customLibrary = {
version: '1.0.0',
init: function () {
/* ... */
},
};
}
/* eslint-enable hub/limit-data-scope */

✅ Instead of Global Modifications

// Use proper module exports/imports
export const myUtility = {
customProperty: 123,
debug: true,
};

// Or use proper configuration patterns
const config = {
apiUrl: process.env.API_URL || 'https://api.example.com',
debug: process.env.NODE_ENV !== 'production',
};

✅ Instead of Broad Scoping

// Move variables to narrowest scope
function processData() {
const localData = { processed: false }; // Declared where used

if (someCondition) {
localData.processed = true;
console.log(localData);
}

return localData;
}

// Use function parameters instead of outer variables
function processItem(item) {
// Parameter instead of module-level variable
return item.value * 2;
}

✅ Instead of var

// Use const for values that won't be reassigned
const PI = 3.14159;
const users = [];

// Use let for values that will be reassigned
let counter = 0;
let currentUser = null;

// Use proper block scoping
if (condition) {
const blockScoped = getValue(); // Won't leak outside block
processValue(blockScoped);
}

Benefits

  • Prevents Namespace Pollution: Avoids conflicts with other code and libraries
  • Improves Code Organization: Encourages proper scoping and separation of concerns
  • Reduces Bugs: Eliminates var-related hoisting issues and accidental global modifications
  • Enhances Maintainability: Makes code easier to understand and refactor
  • Promotes Modern JavaScript: Encourages use of ES6+ features and best practices
  • Better IDE Support: Modern variable declarations provide better IntelliSense and error detection

5. limit-reference-depth

Description: Limits the depth of chained property access and enforces optional chaining to prevent runtime errors. This rule helps avoid brittle code that can crash when encountering null or undefined values in property chains, encouraging safer access patterns and better error handling.

Rationale: Deep chains of property access (e.g., obj.a.b.c.d.e) without proper validation are error-prone and lead to brittle code. Null or undefined values anywhere in the chain can cause runtime crashes, especially in large codebases or when dealing with unpredictable data shapes (e.g., JSON APIs, external configurations). This rule enforces safer patterns by limiting chain depth and requiring optional chaining (?.) or proper null checks, reducing TypeError: Cannot read property 'x' of undefined issues and making code more maintainable.

Options: The rule accepts a single object with the following properties:

maxDepth

  • Type: number
  • Description: Maximum allowed depth for property access chains. A depth of 1 means obj.prop, depth of 2 means obj.prop.subprop, etc.
  • Default: 3
  • Example Usage:
{
"rules": {
"hub/limit-reference-depth": ["warn", { "maxDepth": 2 }]
}
}

requireOptionalChaining

  • Type: boolean
  • Description: When true, requires the use of optional chaining (?.) for all property access beyond the first level.
  • Default: true
  • Example Usage:
{
"rules": {
"hub/limit-reference-depth": ["warn", { "requireOptionalChaining": false }]
}
}

allowSinglePropertyAccess

  • Type: boolean
  • Description: When true, allows single-level property access without optional chaining (e.g., obj.prop is allowed, but obj.prop.subprop still requires obj.prop?.subprop).
  • Default: false
  • Example Usage:
{
"rules": {
"hub/limit-reference-depth": ["warn", { "allowSinglePropertyAccess": true }]
}
}

ignoredBases

  • Type: array of string
  • Description: Array of base identifier names that should be exempt from this rule's checks.
  • Default: []
  • Example Usage:
{
"rules": {
"hub/limit-reference-depth": ["warn", { "ignoredBases": ["config", "env"] }]
}
}

ignoreCallExpressions

  • Type: boolean
  • Description: When true, ignores property chains that end with function calls.
  • Default: true
  • Example Usage:
{
"rules": {
"hub/limit-reference-depth": ["warn", { "ignoreCallExpressions": false }]
}
}

ignoreImportedModules

  • Type: boolean
  • Description: When true, ignores property access on imported/required modules.
  • Default: true
  • Example Usage:
{
"rules": {
"hub/limit-reference-depth": ["warn", { "ignoreImportedModules": false }]
}
}

ignoreGlobals

  • Type: boolean
  • Description: When true, ignores property access on global objects like Math, JSON, console, etc.
  • Default: true
  • Example Usage:
{
"rules": {
"hub/limit-reference-depth": ["warn", { "ignoreGlobals": false }]
}
}

ignoreCommonPatterns

  • Type: boolean
  • Description: When true, ignores common safe patterns like this, super, module, exports, etc.
  • Default: true
  • Example Usage:
{
"rules": {
"hub/limit-reference-depth": ["warn", { "ignoreCommonPatterns": false }]
}
}

Example Configuration

Full Configuration in eslint.config.js:

// eslint.config.js
// Assuming 'hubPlugin' is your imported plugin '@mindfiredigital/eslint-plugin-hub'
{
plugins: {
"hub": hubPlugin,
},
rules: {
"hub/limit-reference-depth": ["warn", {
"maxDepth": 2,
"requireOptionalChaining": true,
"allowSinglePropertyAccess": false,
"ignoredBases": ["config"],
"ignoreCallExpressions": true,
"ignoreImportedModules": true,
"ignoreGlobals": true,
"ignoreCommonPatterns": true
}],
// ... other rules
}
}

Examples

Scenario 1: Default Configuration

"hub/limit-reference-depth": ["warn"] (implies all default options)

✅ Valid (Should NOT produce warnings):

// Optional chaining from the start
const name = item?.details?.name;
const value = obj?.a?.b?.c; // Within maxDepth of 3

// Computed properties with optional chaining
const prop = obj?.[key]?.[subkey];

// Function calls with optional chaining
const result = getUser()?.profile?.name;

// Global objects (ignored by default)
const pi = Math.PI;
const data = JSON.parse(str);

// Import/require usage (ignored by default)
import lodash from 'lodash';
const result = lodash.get(obj, 'path');

// Common patterns (ignored by default)
const value = this.property;
const exp = module.exports;

❌ Invalid (Should PRODUCE warnings):

// Missing optional chaining
const name = item.details.name;
// ESLint Warning: Optional chaining (?.) should be used for accessing property 'details' in 'item.details'.

// Exceeding maxDepth
const deep = obj?.a?.b?.c?.d; // depth 4 > maxDepth 3
// ESLint Warning: Property access chain 'obj?.a?.b?.c?.d' (depth 4) exceeds the maximum allowed depth of 3.

// Mixed optional and non-optional chaining
const mixed = obj?.a.b?.c;
// ESLint Warning: Optional chaining (?.) should be used for accessing property 'b' in 'obj?.a.b'.

// Function calls without optional chaining
const result = getUser().profile.name;
// ESLint Warning: Optional chaining (?.) should be used for accessing property 'profile' in 'getUser().profile'.

Scenario 2: Relaxed Optional Chaining

"hub/limit-reference-depth": ["warn", { "requireOptionalChaining": false }]

✅ Valid (Should NOT produce warnings):

// Regular property access allowed
const name = item.details.name;
const value = obj.a.b.c; // Still within maxDepth

// Mixed patterns allowed
const mixed = obj.a?.b.c;

❌ Invalid (Should PRODUCE warnings):

// Still enforces maxDepth
const deep = obj.a.b.c.d; // depth 4 > maxDepth 3
// ESLint Warning: Property access chain 'obj.a.b.c.d' (depth 4) exceeds the maximum allowed depth of 3.

Scenario 3: Allow Single Property Access

"hub/limit-reference-depth": ["warn", { "allowSinglePropertyAccess": true }]

✅ Valid (Should NOT produce warnings):

// Single property access without optional chaining
const value = obj.prop;

// But deeper access still requires optional chaining
const name = item.details?.name;

❌ Invalid (Should PRODUCE warnings):

// Second level and beyond still need optional chaining
const name = item.details.name;
// ESLint Warning: Optional chaining (?.) should be used for accessing property 'name' in 'item.details.name'.

Scenario 4: Custom maxDepth

"hub/limit-reference-depth": ["warn", { "maxDepth": 2 }]

✅ Valid (Should NOT produce warnings):

// Within maxDepth of 2
const value = obj?.a?.b;

❌ Invalid (Should PRODUCE warnings):

// Exceeds maxDepth of 2
const deep = obj?.a?.b?.c; // depth 3 > maxDepth 2
// ESLint Warning: Property access chain 'obj?.a?.b?.c' (depth 3) exceeds the maximum allowed depth of 2.

Scenario 5: Custom Ignored Bases

"hub/limit-reference-depth": ["warn", { "ignoredBases": ["config", "env"] }]

✅ Valid (Should NOT produce warnings):

// Ignored bases can have deep access
const setting = config.database.connection.host;
const path = env.NODE_ENV.development.settings;

❌ Invalid (Should PRODUCE warnings):

// Non-ignored bases still follow rules
const value = data.nested.deep.property;
// ESLint Warning: Optional chaining (?.) should be used for accessing property 'nested' in 'data.nested'.

Best Practices

// Use optional chaining for safe access
function getItemName(item) {
return item?.details?.name || 'Unnamed Item';
}

// Destructuring with defaults
const { name = 'Unknown' } = item?.details ?? {};

// Early validation
function processUser(user) {
if (!user?.profile?.settings) {
throw new Error('Invalid user data');
}
return user.profile.settings.theme;
}

// Utility functions for complex access
function getNestedValue(obj, path, defaultValue) {
return (
path.split('.').reduce((current, key) => current?.[key], obj) ??
defaultValue
);
}

❌ Patterns to Avoid:

// Deep chains without safety
return item.details.name.value.label; // Brittle, can crash

// Long chains even with optional chaining
return config?.env?.settings?.meta?.internal?.key?.value; // Too complex

// Mixed safe/unsafe patterns
return user?.profile.settings.theme; // Inconsistent safety

6. keep-functions-concise

Description: Enforces a maximum number of lines per function to promote clean, modular code and better maintainability. This rule helps prevent monolithic functions that are hard to read, test, and debug by encouraging developers to break down large functions into smaller, focused, and reusable helper functions.

Rationale: Large, monolithic functions are a common source of technical debt and bugs. They often mix multiple responsibilities, making them difficult to understand, test, and maintain. Functions that span dozens or hundreds of lines become cognitive burdens that slow down development and increase the likelihood of errors. This rule enforces a configurable line limit to encourage separation of concerns, improve code readability, and make functions more testable and maintainable.

Options: The rule accepts a single object with the following properties:

maxLines

  • Type: number
  • Description: Maximum allowed number of lines per function (including function declarations, arrow functions, and function expressions).
  • Default: 60
  • Minimum: 0
  • Example Usage:
{
"rules": {
"hub/keep-functions-concise": ["warn", { "maxLines": 50 }]
}
}

skipBlankLines

  • Type: boolean
  • Description: When true, blank lines are not counted toward the line limit.
  • Default: false
  • Example Usage:
{
"rules": {
"hub/keep-functions-concise": ["warn", { "skipBlankLines": true }]
}
}

skipComments

  • Type: boolean
  • Description: When true, comment-only lines are not counted toward the line limit. This includes single-line comments (//) and single-line block comments (/* */).
  • Default: false
  • Example Usage:
{
"rules": {
"hub/keep-functions-concise": ["warn", { "skipComments": true }]
}
}

Example Configuration

Full Configuration in eslint.config.js:

// eslint.config.js
// Assuming 'hubPlugin' is your imported plugin '@mindfiredigital/eslint-plugin-hub'
{
plugins: {
"hub": hubPlugin,
},
rules: {
"hub/keep-functions-concise": ["warn", {
"maxLines": 60,
"skipBlankLines": true,
"skipComments": true
}],
// ... other rules
}
}

Examples

Scenario 1: Default Configuration

"hub/keep-functions-concise": ["warn"] (implies maxLines: 60, skipBlankLines: false, skipComments: false)

✅ Valid (Should NOT produce warnings):

// Function within line limit
function validateUserData(user) {
if (!user || !user.name) {
return false;
}

if (typeof user.name !== 'string') {
return false;
}

if (user.name.trim().length === 0) {
return false;
}

return true;
}

// Arrow function within limit
const transformUserData = user => {
return {
id: user.id,
name: user.name.toUpperCase(),
email: user.email?.toLowerCase(),
createdAt: new Date().toISOString(),
};
};

// Concise arrow function (single expression)
const getUserId = user => user?.id || null;

// Function expression within limit
const processUser = function (user) {
const isValid = validateUserData(user);
if (!isValid) {
throw new Error('Invalid user data');
}

const transformed = transformUserData(user);
return saveUser(transformed);
};

❌ Invalid (Should PRODUCE warnings):

// Function exceeding line limit (assumes > 60 lines)
function processUserWithEverything(user) {
// Validation logic (15 lines)
if (!user) throw new Error('User is required');
if (!user.name) throw new Error('Name is required');
if (!user.email) throw new Error('Email is required');
if (typeof user.name !== 'string') throw new Error('Name must be string');
if (typeof user.email !== 'string') throw new Error('Email must be string');
if (user.name.trim().length === 0) throw new Error('Name cannot be empty');
if (!user.email.includes('@')) throw new Error('Invalid email format');

// Transformation logic (20 lines)
const normalizedName = user.name.trim().toLowerCase();
const normalizedEmail = user.email.trim().toLowerCase();
const slug = normalizedName.replace(/\s+/g, '-');
const initials = normalizedName
.split(' ')
.map(n => n[0])
.join('')
.toUpperCase();

// Persistence logic (15 lines)
const existingUser = database.users.findByEmail(normalizedEmail);
if (existingUser) {
database.users.update(existingUser.id, {
name: normalizedName,
slug: slug,
initials: initials,
updatedAt: new Date(),
});
} else {
database.users.create({
name: normalizedName,
email: normalizedEmail,
slug: slug,
initials: initials,
createdAt: new Date(),
});
}

// Logging and cleanup (10+ more lines)...
}
// ESLint Warning: Function "processUserWithEverything" has 85 lines (max 60 allowed). (no lines skipped by options)

Scenario 2: Skip Blank Lines

"hub/keep-functions-concise": ["warn", { "skipBlankLines": true }]

✅ Valid (Should NOT produce warnings):

// Function with many blank lines for readability
function calculateTotalPrice(items) {
let subtotal = 0;

for (const item of items) {
subtotal += item.price * item.quantity;
}

const taxRate = 0.08;
const tax = subtotal * taxRate;

const shippingCost = subtotal > 100 ? 0 : 10;

return {
subtotal,
tax,
shipping: shippingCost,
total: subtotal + tax + shippingCost,
};
}
// Blank lines are not counted, so this stays within limits

Scenario 3: Skip Comments

"hub/keep-functions-concise": ["warn", { "skipComments": true }]

✅ Valid (Should NOT produce warnings):

// Well-documented function with many comment lines
function complexBusinessLogic(data) {
// Step 1: Validate input data
// This is critical for preventing downstream errors
if (!data || typeof data !== 'object') {
throw new Error('Invalid input data');
}

// Step 2: Initialize processing variables
// We need these for the calculation loop
let result = 0;
let processed = 0;

// Step 3: Process each item in the data
// The algorithm here implements the XYZ business rule
for (const item of data.items) {
// Skip invalid items to prevent corruption
if (!item.value || item.value < 0) {
continue;
}

// Apply the business transformation
// This formula was provided by the business team
result += item.value * 1.5;
processed++;
}

// Step 4: Apply final adjustments
// These adjustments are required by regulation ABC
if (processed > 10) {
result *= 0.95; // Volume discount
}

/* Final validation before return */
return Math.round(result * 100) / 100;
}
// Comment lines are not counted toward the limit

Scenario 4: Combined Options

"hub/keep-functions-concise": ["warn", { "maxLines": 30, "skipBlankLines": true, "skipComments": true }]

✅ Valid (Should NOT produce warnings):

// Shorter limit but with generous skipping
function moderateFunction(input) {
// This function has a lower line limit
// but comments and blank lines don't count

const step1 = processStep1(input);

// Intermediate processing
const step2 = processStep2(step1);

// Final transformation
return finalizeResult(step2);
}

Scenario 5: Zero Line Limit (Extreme)

"hub/keep-functions-concise": ["error", { "maxLines": 0 }]

✅ Valid (Should NOT produce warnings):

// Only concise arrow functions allowed
const add = (a, b) => a + b;
const getName = user => user?.name || 'Anonymous';
const isValid = data => data && data.length > 0;

❌ Invalid (Should PRODUCE warnings):

// Any function with a block body violates maxLines: 0
function greet(name) {
return `Hello, ${name}!`;
}
// ESLint Warning: Function "greet" has 1 lines (max 0 allowed). (no lines skipped by options)

const multiply = (a, b) => {
return a * b;
};
// ESLint Warning: Function "[anonymous_function]" has 1 lines (max 0 allowed). (no lines skipped by options)
// Break down large functions into focused helpers
function validateUser(user) {
if (!user) throw new Error('User is required');
if (!user.name) throw new Error('Name is required');
if (!user.email) throw new Error('Email is required');
return true;
}

function transformUser(user) {
return {
name: user.name.trim().toLowerCase(),
email: user.email.trim().toLowerCase(),
slug: user.name.replace(/\s+/g, '-'),
};
}

function saveUser(userData) {
return database.users.create({
...userData,
createdAt: new Date(),
});
}

// Main function orchestrates the helpers
function processUser(user) {
validateUser(user);
const transformed = transformUser(user);
return saveUser(transformed);
}

// Use meaningful function names that describe purpose
function calculateShippingCost(subtotal, location) {
if (subtotal > 100) return 0;
return location === 'domestic' ? 10 : 25;
}

// Extract complex conditions into named functions
function isEligibleForDiscount(user, order) {
return user.isPremium && order.total > 200;
}

function processOrder(user, order) {
if (isEligibleForDiscount(user, order)) {
order.total *= 0.9;
}
return order;
}

❌ Patterns to Avoid:

// Monolithic function doing everything
function handleUserRegistration(userData) {
// 50+ lines of validation logic
// 30+ lines of data transformation
// 20+ lines of database operations
// 15+ lines of email sending
// 10+ lines of logging and cleanup
// This function is doing too many things!
}

// Overly long functions even with good structure
function complexCalculation(input) {
// Even if well-organized, 100+ lines in one function
// is usually a sign that it should be broken down
// into smaller, testable pieces
}

// Functions with unclear responsibilities
function doEverything(data) {
// When the function name doesn't clearly indicate
// what it does, it's often too complex
}

Benefits

  • Improved Readability: Shorter functions are easier to understand at a glance
  • Better Testability: Small functions with single responsibilities are easier to unit test
  • Reduced Bugs: Less code per function means fewer places for bugs to hide
  • Enhanced Maintainability: Changes to small functions have limited blast radius
  • Code Reusability: Well-factored helper functions can often be reused elsewhere
  • Easier Code Reviews: Reviewers can more easily understand and verify small functions
  • Better Separation of Concerns: Forces developers to think about function responsibilities

7. use-runtime-assertions

Description: Enforces the presence of a minimum number of runtime assertions in functions to validate inputs and critical intermediate values. This rule helps prevent bugs by encouraging defensive programming practices and proper input validation.

Rationale: Functions without proper input validation are a common source of runtime errors and security vulnerabilities. Unvalidated inputs can lead to unexpected behavior, crashes, or even security exploits. Runtime assertions serve as guardrails that catch invalid data early, making code more robust and easier to debug. They also serve as executable documentation that clarifies the expected behavior and constraints of functions.

This rule enforces:

  • Minimum number of runtime assertions per function
  • Support for various assertion patterns (if-throw, console.assert, custom utilities)
  • Configurable assertion requirements based on function complexity

Example Configuration

{
"rules": {
"hub/use-runtime-assertions": ["warn", {
"minAssertions": 2,
"assertionUtilityNames": ["assert", "invariant", "check"],
"ignoreEmptyFunctions": true
}]
}
}

Recognized Assertion Patterns

The rule recognizes these patterns as runtime assertions:

  1. If-throw statements: if (condition) throw new Error(...)
  2. Direct throw statements: throw new Error(...)
  3. Console assertions: console.assert(condition, message) (when 'assert' is in assertionUtilityNames)
  4. Custom assertion utilities: Any function call matching names in assertionUtilityNames

Examples

✅ Valid Code (Should NOT produce warnings)

Default Configuration (minAssertions: 2)

// Function with proper input validation
function calculate(price, rate) {
if (typeof price !== 'number') throw new Error('Invalid price');
if (typeof rate !== 'number') throw new Error('Invalid rate');
return price * rate;
}

// Mixed assertion types
function processData(data) {
console.assert(data !== null, 'Data cannot be null');
if (!data.id) {
throw new Error('Data must have an ID');
}
return data.processed;
}

// Nested if-throw patterns
function checkUser(user) {
if (!user) throw new Error('User undefined');
console.assert(user.active, 'User must be active');
}

// Arrow function with assertions
const arrowAssert = val => {
if (!val) throw new Error('No val');
console.assert(val > 0, 'Val not positive');
return val * 2;
};

// Function expression with assertions
const exprAssert = function (val) {
if (!val) throw new Error('No val');
console.assert(val > 0, 'Val not positive');
return val;
};

// Empty function (ignored by default)
function noBody() {}

// Arrow function with implicit return
const implicit = a => a + 1;

Custom minAssertions: 1

// Single assertion is sufficient
function simpleCheck(value) {
if (value < 0) throw new Error('Value must be non-negative');
return Math.sqrt(value);
}

Custom Assertion Utilities

// Using custom assertion utility
function customAssertTest(a, b) {
myCustomAssert(typeof a === 'string', 'A must be a string');
if (b < 0) {
throw new Error('B must be positive');
}
return a.repeat(b);
}

Complex Nested Assertions

// Nested if-throw counts as assertion
function nestedIfThrow(value) {
if (value === null) {
if (true) {
// nested condition
throw new Error('Value is critically null');
}
}
if (value < 0) {
throw new Error('Value is negative');
}
return value;
}

❌ Invalid Code (Should PRODUCE warnings)

Default Configuration (minAssertions: 2)

// ESLint Error: Function "calculate" should have at least 2 runtime assertions, but found 1.
function calculate(price, rate) {
if (typeof price !== 'number') throw new Error('Invalid price');
// Only one assertion - needs another
return price * rate;
}

// ESLint Error: Function "noAsserts" should have at least 2 runtime assertions, but found 0.
function noAsserts(value) {
return value * 2;
}

// ESLint Error: Function "calculateDiscount" should have at least 2 runtime assertions, but found 0.
function calculateDiscount(price, discountRate) {
// No input or output validation
return price - price * discountRate;
}

Custom minAssertions: 3

// ESLint Error: Function "needsThree" should have at least 3 runtime assertions, but found 2.
function needsThree(a, b, c) {
if (!a) throw new Error('a is required');
console.assert(b, 'b is required');
// Only two assertions, but needs three
return a + b + c;
}

Custom Assertion Utilities Not Recognized

// When assertionUtilityNames: ['myOrgChecker'] (doesn't include 'assert')
// ESLint Error: Function "usesWrongAssert" should have at least 2 runtime assertions, but found 1.
function usesWrongAssert(value) {
// console.assert not counted because 'assert' not in assertionUtilityNames
console.assert(value, 'Value is present');
if (value < 0) throw new Error('Negative'); // Only this counts
return value;
}

Arrow Functions

// ESLint Error: Function "arrowNoAssert" should have at least 2 runtime assertions, but found 0.
const arrowNoAssert = val => {
return val;
};

Empty Functions (when ignoreEmptyFunctions: false)

// ESLint Error: Function "empty" should have at least 2 runtime assertions, but found 0.
function empty() {}

Configuration Examples

Strict Validation (3+ assertions)

{
"rules": {
"hub/use-runtime-assertions": ["error", {
"minAssertions": 3,
"assertionUtilityNames": ["assert", "invariant", "check"],
"ignoreEmptyFunctions": true
}]
}
}

Minimal Validation (1 assertion)

{
"rules": {
"hub/use-runtime-assertions": ["warn", {
"minAssertions": 1,
"assertionUtilityNames": ["assert"],
"ignoreEmptyFunctions": true
}]
}
}

Custom Assertion Libraries

{
"rules": {
"hub/use-runtime-assertions": ["warn", {
"minAssertions": 2,
"assertionUtilityNames": ["invariant", "check", "validate", "ensure"],
"ignoreEmptyFunctions": true
}]
}
}

No Empty Function Exceptions

{
"rules": {
"hub/use-runtime-assertions": ["warn", {
"minAssertions": 2,
"assertionUtilityNames": ["assert"],
"ignoreEmptyFunctions": false
}]
}
}

✅ Input Validation

function processUser(user, options) {
// Validate required parameters
if (!user) throw new Error('User is required');
if (typeof user.id !== 'string') throw new Error('User ID must be string');

// Validate optional parameters
if (options && typeof options !== 'object') {
throw new Error('Options must be object');
}

return {
id: user.id,
name: user.name,
settings: options || {},
};
}

✅ Boundary Checking

function calculatePercentage(value, total) {
if (typeof value !== 'number' || typeof total !== 'number') {
throw new Error('Both value and total must be numbers');
}
if (total === 0) throw new Error('Total cannot be zero');
if (value < 0 || total < 0) throw new Error('Values must be non-negative');

return (value / total) * 100;
}

✅ State Validation

function withdrawFunds(account, amount) {
if (!account) throw new Error('Account is required');
if (typeof amount !== 'number' || amount <= 0) {
throw new Error('Amount must be positive number');
}
if (account.balance < amount) {
throw new Error('Insufficient funds');
}

account.balance -= amount;
return account.balance;
}

✅ Using Custom Assertion Utilities

// With custom assertion utility
function complexCalculation(data) {
invariant(data && typeof data === 'object', 'Data must be object');
invariant(Array.isArray(data.items), 'Data.items must be array');
check(data.items.length > 0, 'Items array cannot be empty');

return data.items.reduce((sum, item) => sum + item.value, 0);
}

When to Disable

Consider disabling this rule for:

/* eslint-disable hub/use-runtime-assertions */
// Simple utility functions with obvious behavior
function add(a, b) {
return a + b;
}

// Functions that are already validated by TypeScript
function typedFunction(value: NonNullable<string>): string {
return value.toUpperCase();
}

// Test helper functions
function createMockUser() {
return { id: '123', name: 'Test User' };
}
/* eslint-enable hub/use-runtime-assertions */

Alternative Approaches

Instead of disabling the rule, consider:

Lower minAssertions for Simple Functions

{
"rules": {
"hub/use-runtime-assertions": ["warn", { "minAssertions": 1 }]
}
}

Use Type Checking + Runtime Validation

function processData(data: unknown) {
// Runtime validation for dynamic data
if (!data || typeof data !== 'object') {
throw new Error('Invalid data format');
}

if (!('id' in data) || typeof data.id !== 'string') {
throw new Error('Missing or invalid ID');
}

return data as { id: string };
}

Benefits

  • Early Error Detection: Catches invalid inputs before they cause problems
  • Better Debugging: Clear error messages help identify issues quickly
  • Executable Documentation: Assertions serve as live documentation of function requirements
  • Defensive Programming: Encourages thinking about edge cases and error conditions
  • Runtime Safety: Provides guardrails that TypeScript can't offer for dynamic data
  • Improved Reliability: Reduces likelihood of silent failures and unexpected behavior
  • Better Testing: Assertions help identify test cases and boundary conditions

8. minimize-deep-asynchronous-chains

Description: Limits the depth of Promise chains and the number of await expressions in async functions to prevent overly complex asynchronous code that is difficult to read, debug, and maintain.

Rationale: Deep Promise chains and functions with excessive await expressions create several problems: they are harder to understand and debug, make error handling more complex, can lead to callback hell-like patterns even with modern async/await syntax, and often indicate that code should be refactored into smaller, more focused functions. By limiting chain depth and await count, this rule encourages better code organization and maintainability.

This rule enforces:

  • Maximum number of chained Promise methods (.then, .catch, .finally)
  • Maximum number of await expressions per async function
  • Configurable limits for both Promise chains and await expressions

Default Configuration

  • maxPromiseChainLength: 3 (maximum chained .then/.catch/.finally calls)
  • maxAwaitExpressions: 3 (maximum await expressions per async function)

Examples

✅ Valid Code (Should NOT produce warnings)

Default Configuration (maxPromiseChainLength: 3, maxAwaitExpressions: 3)

// Single .then() call
fetch().then(res => res.json());

// Two chained .then() calls
fetch()
.then(res => res.json())
.then(data => console.log(data));

// Mixed .then(), .catch(), .finally() - exactly at limit
fetch().then().catch().finally();

// Promise with .then() and .catch()
promise.then(a => a).catch(err => console.error(err));

// new Promise with chain
new Promise(resolve => resolve()).then(x => x).finally(() => {});

// Nested promise chains within limits
functionA()
.then(() => {
return functionB().then(res => res); // Inner chain length 1
})
.then(final => console.log(final)); // Outer chain length 2

// Single await
async function foo() {
await p1;
}

// Two awaits
async function foo() {
await p1;
await p2;
}

// Three awaits - at limit
async function foo() {
await p1;
await p2;
await p3;
}

// Conditional await within limits
async function bar() {
await step1();
if (condition) {
await step2(); // Still in same function scope
}
await step3();
}

// Nested async functions - each within limits
async function outer() {
await p1(); // 1 for outer

async function inner() {
await p2(); // 1 for inner (separate function)
await p3(); // 2 for inner
await p4(); // 3 for inner
}

await inner(); // 2 for outer
await something(); // 3 for outer
}

// Async arrow function with 3 awaits
const asyncArrow = async () => {
const a = await op1();
const b = await op2(a);
return await op3(b);
};

// Awaits with intermediate variables
async function test() {
const promise = createPromise();
// await in different statements
const a = await promise;
const b = await processA(a);
const c = await processB(b);
}

// Non-promise method chaining should be ignored
func1().func2().func3().func4();

Custom Configuration Examples

// Chain of 4 allowed with custom config
// maxPromiseChainLength: 4
fetch().then().then().then().then();

// 4 awaits allowed with custom config
// maxAwaitExpressions: 4
async function foo() {
await p1;
await p2;
await p3;
await p4;
}

// Different limits for promises vs awaits
// maxPromiseChainLength: 5, maxAwaitExpressions: 1
fetch().then().then();
❌ Invalid Code (Should PRODUCE warnings)

Default Configuration (maxPromiseChainLength: 3, maxAwaitExpressions: 3)

// ESLint Error: Promise chain starting at fetch() has 4 .then/.catch/.finally calls, exceeding the maximum of 3.
fetch().then().then().then().then();

// ESLint Error: Promise chain starting at myPromise has 4 .then/.catch/.finally calls, exceeding the maximum of 3.
myPromise.then(a).catch(b).then(c).finally(d);

// ESLint Error: Promise chain starting at getData() has 4 .then/.catch/.finally calls, exceeding the maximum of 3.
api.getData().then().then().then().catch();

// ESLint Error: Promise chain starting at new Promise() has 4 .then/.catch/.finally calls, exceeding the maximum of 3.
new Promise(resolve => resolve(1)).then().then().then().finally();

// ESLint Error: Async function "foo" has 4 await expressions, exceeding the maximum of 3.
async function foo() {
await p1;
await p2;
await p3;
await p4;
}

// ESLint Error: Async function "bar" has 5 await expressions, exceeding the maximum of 3.
const bar = async () => {
await s1;
let x = await s2;
if (x) {
await s3;
}
await s4;
try {
await s5;
} catch (e) {}
};

// ESLint Error: Async function "processData" has 5 await expressions, exceeding the maximum of 3.
async function processData(data) {
const validated = await validateData(data);
const transformed = await transformData(validated);
const enriched = await enrichData(transformed);
const saved = await saveData(enriched);
const notification = await sendNotification(saved);
return notification;
}

// ESLint Error: Async function "originalProblem" has 5 await expressions, exceeding the maximum of 3.
async function originalProblem(id) {
const r1 = await op1(id);
const r2 = await op2(r1);
const r3 = await op3(r2);
const r4 = await op4(r3);
const r5 = await op5(r4);
return r5;
}

// ESLint Error: Promise chain starting at fetch() has 4 .then/.catch/.finally calls, exceeding the maximum of 3.
fetch('/api/complex-data')
.then(response => response.json())
.then(data => firstProcessing(data))
.then(intermediate => secondProcessing(intermediate))
.then(finalData => displayResult(finalData));

Custom Configuration Violations

// maxPromiseChainLength: 2
// ESLint Error: Promise chain starting at fetch() has 3 .then/.catch/.finally calls, exceeding the maximum of 2.
fetch().then().then().then();

// maxAwaitExpressions: 2
// ESLint Error: Async function "foo" has 3 await expressions, exceeding the maximum of 2.
async function foo() {
await p1;
await p2;
await p3;
}

// maxPromiseChainLength: 10, maxAwaitExpressions: 3
// ESLint Error: Async function "complexFlow" has 4 await expressions, exceeding the maximum of 3.
async function complexFlow() {
await step1();
await step2();
await step3();
await step4();
}

Mixed Violations (Both Types in Same Function)

// ESLint Error: Async function "mixedViolations" has 4 await expressions, exceeding the maximum of 3.
// ESLint Error: Promise chain starting at fetch() has 4 .then/.catch/.finally calls, exceeding the maximum of 3.
async function mixedViolations() {
// This function has too many awaits
await step1();
await step2();
await step3();
await step4();

// And also creates a long promise chain
return fetch('/data')
.then(res => res.json())
.then(data => process1(data))
.then(result => process2(result))
.then(final => final);
}

Configuration Examples

Strict Limits (2 max for both)

{
"rules": {
"hub/minimize-deep-asynchronous-chains": ["error", {
"maxPromiseChainLength": 2,
"maxAwaitExpressions": 2
}]
}
}

Relaxed Limits (5 max for both)

{
"rules": {
"hub/minimize-deep-asynchronous-chains": ["warn", {
"maxPromiseChainLength": 5,
"maxAwaitExpressions": 5
}]
}
}

Mixed Limits (Different for promises vs awaits)

{
"rules": {
"hub/minimize-deep-asynchronous-chains": ["warn", {
"maxPromiseChainLength": 4,
"maxAwaitExpressions": 2
}]
}
}

Only Check Promise Chains

{
"rules": {
"hub/minimize-deep-asynchronous-chains": ["warn", {
"maxPromiseChainLength": 3,
"maxAwaitExpressions": 999
}]
}
}

✅ Refactor Long Promise Chains

// Instead of this (violates rule):
fetch('/api/data')
.then(response => response.json())
.then(data => validateData(data))
.then(validData => processData(validData))
.then(processedData => saveData(processedData));

// Do this:
async function handleDataFlow() {
const response = await fetch('/api/data');
const data = await response.json();
const validData = await validateData(data);
const processedData = await processData(validData);
return await saveData(processedData);
}

// Or extract helper functions:
async function fetchAndParseData() {
const response = await fetch('/api/data');
return await response.json();
}

async function processAndSaveData(data) {
const validData = await validateData(data);
const processedData = await processData(validData);
return await saveData(processedData);
}

async function handleDataFlow() {
const data = await fetchAndParseData();
return await processAndSaveData(data);
}

✅ Break Down Complex Async Functions

// Instead of this (violates rule):
async function complexOperation(id) {
const user = await fetchUser(id);
const profile = await fetchProfile(user.profileId);
const settings = await fetchSettings(user.settingsId);
const permissions = await fetchPermissions(user.roleId);
const activities = await fetchActivities(user.id);
return { user, profile, settings, permissions, activities };
}

// Do this:
async function fetchUserData(id) {
const user = await fetchUser(id);
const profile = await fetchProfile(user.profileId);
const settings = await fetchSettings(user.settingsId);
return { user, profile, settings };
}

async function fetchUserMetadata(user) {
const permissions = await fetchPermissions(user.roleId);
const activities = await fetchActivities(user.id);
return { permissions, activities };
}

async function complexOperation(id) {
const userData = await fetchUserData(id);
const metadata = await fetchUserMetadata(userData.user);
return { ...userData, ...metadata };
}

✅ Use Promise.all for Parallel Operations

// Instead of sequential awaits:
async function fetchAllData(ids) {
const results = [];
for (const id of ids) {
const result = await fetchData(id);
results.push(result);
}
return results;
}

// Use parallel execution:
async function fetchAllData(ids) {
const promises = ids.map(id => fetchData(id));
return await Promise.all(promises);
}

When to Disable

Consider disabling this rule for:

/* eslint-disable hub/minimize-deep-asynchronous-chains */
// Complex orchestration functions that legitimately need many async operations
async function orchestrateComplexWorkflow(data) {
// This function coordinates a complex multi-step process
const validated = await validateInput(data);
const prepared = await prepareData(validated);
const processed = await processStep1(prepared);
const enhanced = await processStep2(processed);
const finalized = await processStep3(enhanced);
const result = await finalizeResult(finalized);
return result;
}

// Legacy code migration where immediate refactoring isn't feasible
fetch('/legacy-api')
.then(response => response.json())
.then(data => legacyTransform1(data))
.then(data => legacyTransform2(data))
.then(data => legacyTransform3(data))
.then(data => legacyOutput(data));
/* eslint-enable hub/minimize-deep-asynchronous-chains */

Alternative Approaches

Instead of disabling the rule, consider:

Higher Limits for Specific Cases

{
"rules": {
"hub/minimize-deep-asynchronous-chains": ["warn", {
"maxPromiseChainLength": 5,
"maxAwaitExpressions": 6
}]
}
}

Functional Composition

// Use functional composition for complex transformations
const pipe =
(...fns) =>
value =>
fns.reduce((acc, fn) => fn(acc), value);

const processData = pipe(validateData, transformData, enrichData);

async function handleData(rawData) {
const processed = await processData(rawData);
return await saveData(processed);
}

Benefits

  • Improved Readability: Shorter chains and functions are easier to understand
  • Better Error Handling: Fewer nested operations make error handling clearer
  • Enhanced Debugging: Smaller async operations are easier to debug and test
  • Encourages Composition: Promotes breaking down complex operations into smaller, reusable functions
  • Prevents Callback Hell: Even with modern async/await, excessive nesting creates similar problems
  • Better Maintainability: Smaller, focused async functions are easier to modify and extend
  • Performance Awareness: Encourages thinking about whether operations can be parallelized
  • Code Organization: Forces developers to think about proper function boundaries and responsibilities

9. check-return-values

Description: Enforces handling of return values from non-void functions. If a function's return value is intentionally not used, it should be explicitly ignored via void operator, assignment to an underscore (_), or a specific comment. This rule helps prevent bugs caused by unintentionally overlooking important results from function calls, such as error flags, success statuses, or computed data. It exempts standard console.* method calls.

Rationale: Neglecting to check or use the return value of a function can lead to silent failures or missed opportunities to act on critical information. For instance, a function that updates a database might return a success/failure status; ignoring this status means the application might proceed unaware of an error. This rule encourages developers to be deliberate about function outcomes, improving code robustness and reliability.

  • Example Usage:
{
"rules": {
"hub/check-return-values": ["warn"]
}
}

Example of Full Configuration in eslint.config.js:

// eslint.config.js
import hub from '@mindfiredigital/eslint-plugin-hub';

export default [
{
plugins: { hub: hub },
rules: {
'hub/check-return-values': ['error'],
// ... other rules
},
},
];

Examples:

Scenario 1: Default Configuration (requireExplicitIgnore: true)

"hub/check-return-values": ["warn"]

✅ Valid:

function doSomething() {
return 42;
}
const result = doSomething(); // Value used

let success;
success = doSomething(); // Value used

if (doSomething()) {
/* Value used */
}

function another() {
return doSomething();
} // Value used

void doSomething(); // Explicitly ignored

_ = doSomething(); // Explicitly ignored

// return value intentionally ignored
doSomething();

doSomething(); // return value intentionally ignored

/* return value intentionally ignored */
doSomething();

console.log('Hello'); // Console calls are exempt

❌ Invalid:

function doSomething() {
return true;
}

doSomething(); // Value not used

Best Practices for Node.js Project Reliability

By following these rules, you can ensure a more robust and reliable Node.js codebase. Node.js projects benefit from proactive error prevention, particularly when adhering to these development practices:

  • Prevent infinite loops with clear termination conditions: This avoids application hangs and resource exhaustion that can crash your Node.js server.
  • Address linter warnings immediately: This prevents technical debt accumulation and catches potential bugs before they reach production.
  • Maintain proper data scoping: This reduces global pollution and naming conflicts that are especially problematic in Node.js's module system.

Additional Notes

  • Customization: If necessary, you can override these rules to fit the specific needs of your Node.js project. However, adhering to these practices is highly recommended for production stability and maintainability.
  • Performance Impact: These rules help prevent common Node.js performance pitfalls like infinite loops and memory leaks from improper scoping.
  • Production Readiness: Following these guidelines ensures your Node.js applications are more suitable for production environments where reliability is critical.