DEV Community

Sam Abaasi
Sam Abaasi

Posted on

Avoiding Unsafe Calls in JavaScript and React Projects with ESLint

Avoiding Unsafe Calls in JavaScript and React Projects with ESLint

๐Ÿ“œโœจ In modern JavaScript and React applications, it's common to encounter runtime errors caused by accessing properties on undefined or null values, or calling methods on undefined arrays or objects. These issues can disrupt user experience and make debugging a nightmare. In this article, we'll identify the common issues and provide an ESLint configuration to mitigate them effectively. ๐ŸŒŸ๐Ÿ’ป

๐ŸŒŸโœจ In our React or React Native projects, because we are not using TypeScript, we sometimes forget to write safe code. These unsafe codes can lead to many issues in production, like crashing React Native apps, frustrating users, and complicating maintenance. Let's dive into these common issues and how to solve them. ๐Ÿšจ๐Ÿš€๐Ÿ’”

Common Issues with Unsafe Calls

1. Accessing Properties on undefined or null ๐ŸŒŸ๐Ÿ”โœจ

Problem:

Accessing a property on an object that is undefined or null causes a runtime error:

const user = undefined;
console.log(user.name); // โŒ Runtime Error: Cannot read property 'name' of undefined
Enter fullscreen mode Exit fullscreen mode

Solution:

  • Solution 1: Use optional chaining (?.).
const name = user?.name;
Enter fullscreen mode Exit fullscreen mode
  • Solution 2: Employ a default fallback.
const name = user ? user.name : 'Default Name';
Enter fullscreen mode Exit fullscreen mode
  • Solution 3: Ensure initialization before access.
const user = { name: 'Default Name' };
Enter fullscreen mode Exit fullscreen mode

2. Calling Methods on undefined or null โœ‹๐Ÿ“‰๐Ÿ“‹

Problem:

Calling methods like .map() or .filter() on an undefined array throws an error:

const items = undefined;
items.map((item) => console.log(item)); // โŒ Runtime Error: Cannot read property 'map' of undefined
Enter fullscreen mode Exit fullscreen mode

Solution:

  • Solution 1: Verify that the variable is an array.
if (Array.isArray(items)) {
  items.map(item => console.log(item));
}
Enter fullscreen mode Exit fullscreen mode
  • Solution 2: Provide a default array.
const items = someValue || [];
items.map(item => console.log(item));
Enter fullscreen mode Exit fullscreen mode
  • Solution 3: Use the nullish coalescing operator (??).
const items = possibleItems ?? [];
Enter fullscreen mode Exit fullscreen mode

3. Invoking Undefined Functions โš™๏ธโš ๏ธ๐Ÿšซ

Problem:

Trying to call a function that might be undefined:

const handler = undefined;
handler(); // โŒ Runtime Error: handler is not a function
Enter fullscreen mode Exit fullscreen mode

Solution:

  • Solution 1: Check existence before invocation.
if (typeof handler === 'function') {
  handler();
}
Enter fullscreen mode Exit fullscreen mode
  • Solution 2: Assign a no-op default function.
const handler = passedHandler || (() => {});
Enter fullscreen mode Exit fullscreen mode

4. Destructuring Undefined or Null Objects ๐Ÿ“ฆโŒ๐Ÿ”“

Problem:

Destructuring properties from an undefined object results in an error:

const obj = undefined;
const { name } = obj; // โŒ Runtime Error: Cannot destructure property 'name' of undefined
Enter fullscreen mode Exit fullscreen mode

Solution:

  • Solution 1: Use optional chaining with defaults.
const { name = 'Default Name' } = user || {};
Enter fullscreen mode Exit fullscreen mode
  • Solution 2: Validate before destructuring.
if (user) {
  const { name } = user;
}
Enter fullscreen mode Exit fullscreen mode

5. Accessing Non-Existent Array Elements ๐Ÿšซ๐Ÿ“‹๐Ÿ“Š

Problem:

Accessing elements of an undefined array causes an error:

const arr = undefined;
const first = arr[0]; // โŒ Runtime Error: Cannot read property '0' of undefined
Enter fullscreen mode Exit fullscreen mode

Solution:

  • Solution 1: Provide a default fallback.
const first = (arr && arr[0]) || 'Default Value';
Enter fullscreen mode Exit fullscreen mode
  • Solution 2: Initialize arrays properly.
const arr = arr || [];
const first = arr[0];
Enter fullscreen mode Exit fullscreen mode

6. Invalid Array/Function Usage ๐Ÿ›‘๐Ÿ“

Problem:

Using array methods like .map() or .filter() on undefined values or objects:

const numbers = undefined;
numbers.filter((n) => n > 0); // โŒ Runtime Error: Cannot read property 'filter' of undefined
Enter fullscreen mode Exit fullscreen mode

Solution:

  • Solution 1: Always validate inputs for array functions.
if (Array.isArray(numbers)) {
  numbers.filter(n => n > 0);
}
Enter fullscreen mode Exit fullscreen mode
  • Solution 2: Return a safe result if input isn't valid.
const results = Array.isArray(numbers) ? numbers.filter(n => n > 0) : [];
Enter fullscreen mode Exit fullscreen mode

7. Insufficient Conditional Checks ๐Ÿ’ก๐Ÿ”’

Problem:

Failing to validate conditions strictly can lead to bugs, such as relying on falsy values. For example, if conditions expecting a boolean might incorrectly evaluate other types like undefined or 0:

const obj = {};
if (obj.prop) {
  // Executes even if obj.prop is `undefined`, leading to unexpected behavior
}
Enter fullscreen mode Exit fullscreen mode

Solution:

  • Solution 1: Use strict equality comparisons.
if (obj.prop === true) {
  console.log('Do something');
}
Enter fullscreen mode Exit fullscreen mode
  • Solution 2: Coerce values explicitly for intended behavior.
if (!!obj.prop) {
  console.log('Do something');
}
Enter fullscreen mode Exit fullscreen mode
  • Solution 3: Define explicit conditions in your code.
if (typeof obj.prop === 'boolean' && obj.prop) {
  console.log('Safe conditional execution');
}
Enter fullscreen mode Exit fullscreen mode

Using ESLint to Avoid Unsafe Calls ๐Ÿš€๐Ÿ”’โœ…

To catch these issues during development, we can leverage ESLint with specific rules. Below is an ESLint configuration that will flag unsafe calls and suggest fixes. ๐Ÿ› ๏ธ๐Ÿ”๐ŸŒŸ

ESLint Configuration ๐Ÿ“œ๐Ÿ’ป๐Ÿ›ก๏ธ

Add the following rules to your .eslintrc.js or ESLint configuration file:

module.exports = {
  parser: '@typescript-eslint/parser', // If using TypeScript
  plugins: ['@typescript-eslint'],
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/recommended-requiring-type-checking',
  ],
  parserOptions: {
    ecmaVersion: 2020, // or later
    sourceType: 'module',
    project: './tsconfig.json', // Type-aware linting
  },
  rules: {
    // 1. Disallow accessing properties on undefined/null
    '@typescript-eslint/no-unnecessary-condition': 'warn',

    // 2. Enforce optional chaining where needed
    'no-unused-expressions': ['warn', { allowShortCircuit: true }],

    // 3. Disallow unsafe calls (functions on undefined/null)
    '@typescript-eslint/no-unsafe-call': 'error',

    // 4. Disallow unsafe member access (accessing props on undefined/null)
    '@typescript-eslint/no-unsafe-member-access': 'error',

    // 5. Catch invalid destructuring of undefined or null
    '@typescript-eslint/no-unnecessary-condition': 'warn',

    // 6. Catch invalid array/function usage (e.g., map/filter on undefined)
    'consistent-return': 'warn',

    // 7. Enforce stricter conditional checks
    '@typescript-eslint/strict-boolean-expressions': [
      'error',
      {
        allowNullableObject: false,
        allowNullableBoolean: true,
        allowNullableString: true,
        allowNumber: true,
        allowAny: false,
      },
    ],

    // Additional rules for clarity and safety:
    'no-implicit-coercion': ['warn', { allow: ['!!'] }],
    'no-unreachable': 'error',
    '@typescript-eslint/no-non-null-assertion': 'error',
  },
};
Enter fullscreen mode Exit fullscreen mode

Explanation of Rules ๐Ÿ’กโš™๏ธ๐ŸŒ

  1. @typescript-eslint/no-unnecessary-condition: Flags unnecessary conditions or unhandled potential undefined or null values. โš ๏ธ
  2. no-unused-expressions: Ensures that short-circuited logic like someObject && someObject.doSomething is avoided unless explicitly necessary. ๐Ÿš€
  3. @typescript-eslint/no-unsafe-call: Prevents unsafe function calls on non-functions. โŒ
  4. @typescript-eslint/no-unsafe-member-access: Flags attempts to access properties on potentially undefined values. โœ‹
  5. consistent-return: Enforces consistent return types in functions to avoid returning invalid or undefined values. ๐Ÿ’พ
  6. @typescript-eslint/strict-boolean-expressions: Strengthens conditional expressions by preventing implicit coercion. ๐Ÿ”’
  7. @typescript-eslint/no-non-null-assertion: Disallows the unsafe ! operator used to bypass null/undefined checks. ๐Ÿšซ

Installing Required Dependencies ๐Ÿ“ฆ๐Ÿ”ง๐Ÿ”

To enable these rules, ensure you have the necessary ESLint plugins and parsers installed:

npm install eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev
Enter fullscreen mode Exit fullscreen mode

Integrating ESLint with VSCode ๐Ÿ–ฅ๏ธ๐Ÿšฆ๐ŸŒˆ

1. Install the ESLint Extension:

  • Search for "ESLint" in the VSCode marketplace and install it. โœจ

2. Enable Auto-Fixing:

Add the following to your settings.json:

{
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"]
}
Enter fullscreen mode Exit fullscreen mode

3. Run ESLint:

Add an npm script to run ESLint:

"scripts": {
  "lint": "eslint . --ext .js,.jsx,.ts,.tsx"
}
Enter fullscreen mode Exit fullscreen mode

Then, run npm run lint to catch issues. ๐Ÿš€


Conclusion ๐ŸŒŸ๐Ÿš€๐Ÿ“˜

By implementing the above ESLint rules and practices, you can catch and fix unsafe calls before they become runtime errors. ๐ŸŽ‰๐ŸŒˆ This approach will improve the reliability and maintainability of your JavaScript and React projects. ๐ŸŒโš™๏ธโœจ


Top comments (0)