JavaScript by Example

Events

Attaching listeners, reading the event object, and cleaning up with AbortController to avoid the memory leaks that forgotten removeEventListener leaves behind.

Events are how the browser tells your code that something happened: a click, a key press, a form submission, a window resize. You attach a listener function with addEventListener and the browser calls it with an event object that describes what happened. This lesson runs in a browser; the earlier lessons run anywhere JavaScript runs.

addEventListener takes an event name and a handler function. The handler receives an event object. event.target is the element that was interacted with. event.preventDefault() stops the browser's default behavior - useful for links you want to handle in JavaScript without a page navigation.

const button = document.querySelector("button");
 
button.addEventListener("click", (event) => {
  console.log("clicked:", event.target.textContent);
  // event.target is the element that was clicked
});
 
// Prevent default behavior (e.g. stop a link from navigating)
const link = document.querySelector("a.ajax-link");
link.addEventListener("click", (event) => {
  event.preventDefault();
  console.log("link click intercepted, no navigation");
  // handle it yourself instead
});

The input event fires on every keystroke as the user types. The change event fires when the element loses focus (blur) after its value changed. Use input for live feedback and change for "finished editing" behavior.

const field = document.querySelector("input[type='text']");
 
// Fires on every keystroke
field.addEventListener("input", (event) => {
  console.log("current value:", event.target.value);
});
 
// Fires once when the field loses focus and the value changed
field.addEventListener("change", (event) => {
  console.log("committed value:", event.target.value);
});

AbortController is the modern way to remove a group of listeners at once. Pass the controller's signal to each addEventListener call, then call controller.abort() to remove them all. This is cleaner than tracking and calling removeEventListener for every single handler.

function setupListeners(element) {
  const controller = new AbortController();
  const { signal } = controller;
 
  element.addEventListener("click", () => {
    console.log("clicked");
  }, { signal });
 
  element.addEventListener("keydown", (event) => {
    console.log("key pressed:", event.key);
  }, { signal });
 
  window.addEventListener("resize", () => {
    console.log("window resized");
  }, { signal });
 
  // Later: remove ALL listeners above with a single call
  function cleanup() {
    controller.abort();
  }
 
  return cleanup;
}
 
const removeAll = setupListeners(document.querySelector(".widget"));
// When the component is destroyed:
removeAll();

In production

A forgotten removeEventListener keeps the handler closure alive - along with everything the closure captures, including DOM nodes. This is one of the most common memory leak sources in single-page applications. Use an AbortController per component or lifecycle scope and call .abort() on teardown. The pattern pairs cleanly with framework cleanup hooks (React's useEffect return, Vue's onUnmounted).

Enjoyed this? Get more essays on software craft delivered to your inbox.

Subscribe free