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
Solution:
-
Solution 1: Use optional chaining (
?.
).
const name = user?.name;
- Solution 2: Employ a default fallback.
const name = user ? user.name : 'Default Name';
- Solution 3: Ensure initialization before access.
const user = { name: 'Default Name' };
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
Solution:
- Solution 1: Verify that the variable is an array.
if (Array.isArray(items)) {
items.map(item => console.log(item));
}
- Solution 2: Provide a default array.
const items = someValue || [];
items.map(item => console.log(item));
-
Solution 3: Use the nullish coalescing operator (
??
).
const items = possibleItems ?? [];
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
Solution:
- Solution 1: Check existence before invocation.
if (typeof handler === 'function') {
handler();
}
- Solution 2: Assign a no-op default function.
const handler = passedHandler || (() => {});
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
Solution:
- Solution 1: Use optional chaining with defaults.
const { name = 'Default Name' } = user || {};
- Solution 2: Validate before destructuring.
if (user) {
const { name } = user;
}
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
Solution:
- Solution 1: Provide a default fallback.
const first = (arr && arr[0]) || 'Default Value';
- Solution 2: Initialize arrays properly.
const arr = arr || [];
const first = arr[0];
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
Solution:
- Solution 1: Always validate inputs for array functions.
if (Array.isArray(numbers)) {
numbers.filter(n => n > 0);
}
- Solution 2: Return a safe result if input isn't valid.
const results = Array.isArray(numbers) ? numbers.filter(n => n > 0) : [];
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
}
Solution:
- Solution 1: Use strict equality comparisons.
if (obj.prop === true) {
console.log('Do something');
}
- Solution 2: Coerce values explicitly for intended behavior.
if (!!obj.prop) {
console.log('Do something');
}
- Solution 3: Define explicit conditions in your code.
if (typeof obj.prop === 'boolean' && obj.prop) {
console.log('Safe conditional execution');
}
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',
},
};
Explanation of Rules ๐กโ๏ธ๐
-
@typescript-eslint/no-unnecessary-condition
: Flags unnecessary conditions or unhandled potentialundefined
ornull
values. โ ๏ธ -
no-unused-expressions
: Ensures that short-circuited logic likesomeObject && someObject.doSomething
is avoided unless explicitly necessary. ๐ -
@typescript-eslint/no-unsafe-call
: Prevents unsafe function calls on non-functions. โ -
@typescript-eslint/no-unsafe-member-access
: Flags attempts to access properties on potentiallyundefined
values. โ -
consistent-return
: Enforces consistent return types in functions to avoid returning invalid or undefined values. ๐พ -
@typescript-eslint/strict-boolean-expressions
: Strengthens conditional expressions by preventing implicit coercion. ๐ -
@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
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"]
}
3. Run ESLint:
Add an npm script to run ESLint:
"scripts": {
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
}
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)