ESLint plugin to detect unsafe event handling patterns in asynchronous contexts.
In JavaScript event handlers, event objects and their properties have specific behaviors that can lead to bugs when used in asynchronous contexts:
- Event methods timing: Methods like
preventDefault()
andstopPropagation()
only work when called synchronously. - Event property references: When working with delegated events, frameworks may replace the
event.currentTarget
property as the event bubbles.
When using async
event handlers or promises in event callbacks, accessing these objects or methods after an await
or in promise chains can lead to unexpected behavior or errors.
This plugin provides two rules to catch common mistakes with event handling in async contexts:
- no-async-event-properties: Prevents accessing specific event properties or calling methods after await expressions
- no-async-event-reference: Prevents referencing event objects after await expressions
Install from NPM
npm install eslint-plugin-async-event
Add the plugin to your .eslintrc.js
or eslint.config.js
:
// For flat config (eslint.config.js)
import asyncEventPlugin from 'eslint-plugin-async-event';
export default [
// ... other configs
{
plugins: {
'async-event': asyncEventPlugin,
},
rules: {
// no-async-event-reference is a stronger version of
// no-async-event-properties, so really no need to
// include both, but do what works for you
'async-event/no-async-event-properties': 'error',
'async-event/no-async-event-reference': 'error',
},
},
];
// For .eslintrc.js
module.exports = {
plugins: ['async-event'],
rules: {
// no-async-event-reference is a stronger version of
// no-async-event-properties, so really no need to
// include both, but do what works for you
'async-event/no-async-event-properties': 'error',
'async-event/no-async-event-reference': 'error',
},
};
This plugin exports a recommended configuration that turns on all rules with sensible defaults:
// For flat config (eslint.config.js)
import asyncEventPlugin from 'eslint-plugin-async-event';
export default [
// ... other configs
asyncEventPlugin.configs.recommended,
];
// For .eslintrc.js
module.exports = {
extends: ['plugin:async-event/recommended'],
};
Prevents accessing specific event properties or calling methods after await expressions or in promise chains.
By default, this rule disallows accessing the following properties after an await or in a promise chain:
currentTarget
- This usually references the element to which an event listener is attached, but when working with event delegation, a single handler may be attached to the document that redefinescurrentTarget
as the event bubbles.preventDefault
- Doesn't work if called asynchronously.stopPropagation
- Doesn't work if called asynchronously.stopImmediatePropagation
- Doesn't work if called asynchronously.
You can customize which properties are disallowed by providing a properties array:
{
'async-event/no-async-event-properties': ['error', {
properties: ['currentTarget', 'preventDefault', 'target', 'bubbles']
}]
}
You can also customize how variables are identified as event parameters:
{
'async-event/no-async-event-properties': ['error', {
// Standard properties to check
properties: ['currentTarget', 'preventDefault', 'target', 'bubbles'],
// Configure event parameter detection
eventPatterns: [
'event',
'e',
'ev',
'evt',
'mouseEvent',
'*Event', // wildcard pattern for anything ending with "Event"
'_event*' // wildcard pattern for anything starting with "_event"
]
}]
}
By default, the plugin identifies event parameters using:
- Exact matches for:
event
,e
,ev
- Wildcard pattern:
*Event
(anything ending with "Event")
Wildcard patterns use *
to match any string (including empty string). The *
can appear anywhere in the pattern.
// ❌ Accessing currentTarget after await
async function handleClick(event) {
await fetchData();
console.log(event.currentTarget); // Error: currentTarget may no longer be valid
}
// ❌ Using preventDefault after await
async function handleSubmit(event) {
await validateForm();
event.preventDefault(); // Too late, form already submitted
}
// ❌ Using stopPropagation in promise chain
function handleClick(event) {
fetchData().then(() => {
event.stopPropagation(); // Too late, event already propagated
});
}
// ✅ Storing currentTarget in a variable before await
async function handleClick(event) {
const target = event.currentTarget;
await fetchData();
console.log(target); // Safe: using stored reference
}
// ✅ Using preventDefault before await
async function handleSubmit(event) {
event.preventDefault(); // Correct: called synchronously
await validateForm();
// Process form...
}
// ✅ Using stopPropagation before promise chain
function handleClick(event) {
event.stopPropagation(); // Correct: called synchronously
fetchData().then(() => {
// Handle data...
});
}
// ✅ Accessing properties that aren't in the disallowed list
async function handleInput(event) {
await fetchData();
console.log(event.type); // Safe if 'type' is not in the disallowed properties list
}
Prevents referencing event objects after await expressions or in promise chains. This is essentially a stronger version of no-async-event-properties
to prevent, e.g., asynchronously passing the event
object to some helper function which then calls preventDefault
.
You can customize how variables are identified as event parameters:
{
'async-event/no-async-event-reference': ['error', {
// Configure event parameter detection
eventPatterns: [
'event',
'e',
'ev',
'evt',
'mouseEvent',
'*Event', // wildcard pattern for anything ending with "Event"
'_event*' // wildcard pattern for anything starting with "_event"
]
}]
}
By default, the plugin identifies event parameters using:
- Exact matches for:
event
,e
,ev
,evt
- Wildcard pattern:
*Event
(anything ending with "Event")
Wildcard patterns use *
to match any string (including empty string). The *
can appear anywhere in the pattern.
// ❌ Referencing event object after await
async function handleInput(event) {
await saveData();
// Error: `doSomethingWith` might do something unsafe with event
doSomethingWith(event);
}
// ❌ Using event in promise chain
function handleChange(event) {
fetchOptions().then(() => {
// Error: `updateUI` might do something unsafe with event
updateUI(event);
});
}
// ✅ Store needed values before await
async function handleInput(event) {
const target = event.target;
await saveData();
doSomethingWith(target); // Safe: using stored property
}
// ✅ Capture necessary data before promise chain
function handleChange(event) {
const target = event.target;
fetchOptions().then(() => {
updateUI(target); // Safe: using captured property
});
}
MIT