async programming
await function
event handling
JavaScript
concurrency

Async await and event handler

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

async and await work inside event handlers, but they do not change how event dispatch itself behaves. An event source such as the browser DOM or Node.js EventEmitter does not wait for your returned promise unless you build that behavior yourself. That distinction matters because it affects error handling, UI state, and ordering.

An Event Handler Can Be async

In the browser, this is perfectly valid:

javascript
1const button = document.querySelector("#save");
2
3button.addEventListener("click", async () => {
4  const response = await fetch("/api/save", { method: "POST" });
5  const result = await response.json();
6  console.log(result);
7});

The handler starts when the click occurs. When execution reaches await, JavaScript suspends that handler and lets the event loop continue running other tasks. That makes the code easier to read than chaining .then(...).

What The Event System Does Not Do

The important misconception is this: the event system does not "await the handler." It simply calls the function. If the function returns a promise, the caller usually ignores it.

That means:

  • later code outside the handler does not automatically wait,
  • multiple clicks can start multiple overlapping async operations,
  • thrown errors can become rejected promises if you do not handle them.

So async improves handler code structure, but it does not create serialization or built-in backpressure.

A Safer Browser Pattern

A clean pattern is to keep the event listener small and delegate to an async function with explicit error handling.

javascript
1const button = document.querySelector("#save");
2
3button.addEventListener("click", () => {
4  void handleSaveClick();
5});
6
7async function handleSaveClick() {
8  button.disabled = true;
9
10  try {
11    const response = await fetch("/api/save", { method: "POST" });
12    if (!response.ok) {
13      throw new Error(`Request failed with ${response.status}`);
14    }
15
16    const result = await response.json();
17    console.log("Saved:", result);
18  } catch (error) {
19    console.error("Save failed:", error);
20  } finally {
21    button.disabled = false;
22  }
23}

This makes two things explicit:

  • the listener itself is fire-and-forget,
  • the async workflow owns its own error handling and UI cleanup.

Prevent Overlapping Work

If a user can trigger the same event repeatedly, async handlers can overlap unless you guard against it. Disabling the control is one option. A boolean flag is another.

javascript
1let inFlight = false;
2
3async function handleSaveClick() {
4  if (inFlight) {
5    return;
6  }
7
8  inFlight = true;
9  try {
10    await new Promise(resolve => setTimeout(resolve, 500));
11    console.log("Finished once");
12  } finally {
13    inFlight = false;
14  }
15}

Without this kind of protection, double-clicks can submit the same request twice.

Node.js Events Behave Similarly

Node.js EventEmitter does not await async listeners either.

javascript
1const { EventEmitter } = require("events");
2
3const emitter = new EventEmitter();
4
5emitter.on("job", async id => {
6  await new Promise(resolve => setTimeout(resolve, 100));
7  console.log("Processed", id);
8});
9
10emitter.emit("job", 42);
11console.log("Emit returned immediately");

emit returns before the awaited work finishes. If you need sequential async handling, you have to build it yourself instead of assuming the emitter will manage it.

When Ordering Really Matters

If one event must finish before the next begins, introduce an explicit queue.

javascript
1let current = Promise.resolve();
2
3function enqueue(task) {
4  current = current.then(task, task);
5  return current;
6}
7
8button.addEventListener("click", () => {
9  void enqueue(async () => {
10    await new Promise(resolve => setTimeout(resolve, 300));
11    console.log("Handled in order");
12  });
13});

This pattern is often better than trying to force event dispatch itself to become await-aware.

Common Pitfalls

  • Assuming the event source waits for the promise returned by an async handler.
  • Forgetting try and catch, which can leave rejected promises unhandled.
  • Allowing repeated events to start overlapping async work accidentally.
  • Writing UI code that never resets disabled state if an awaited call throws.
  • Expecting Node.js emit to serialize async listeners automatically.

Summary

  • Event handlers can be async, and await works normally inside them.
  • The event system usually ignores the returned promise.
  • Use explicit error handling and cleanup inside async handlers.
  • Guard against overlapping work when the same event can fire repeatedly.
  • Build queues or locks yourself when ordering matters.

Course illustration
Course illustration

All Rights Reserved.