Skip to main content

await inside an event handler

Yes, you can use await inside an event handler, but the handler itself has to be declared async. It is an ordinary async function that the browser calls: it returns a promise nobody waits for, so the UI is never blocked, and handling errors is entirely your responsibility.

Theory

TL;DR

  • await is allowed only inside a function marked async, otherwise the engine throws a SyntaxError.
  • The working form is element.addEventListener('click', async () => { ... }).
  • The browser does not wait for the promise the handler returns and has no idea there is an await inside.
  • Errors from an async handler do not join the normal exception flow, so you need try/catch.
  • Call preventDefault() and stopPropagation() synchronously, before the first await.
  • It is convenient to move the logic into a separate async function and pass it as the handler.

Quick example

javascript
// Wrong: await inside a plain, non-async function. button.addEventListener('click', () => { await fetch('/api/data'); // SyntaxError }); // Correct: the handler itself is async. button.addEventListener('click', async () => { const response = await fetch('/api/data'); const data = await response.json(); console.log(data); });

Now everything is legal: inside the handler execution waits for fetch, but the event itself does not freeze the page, and the rest of the code keeps running.

The basic rule: await lives only inside async

await is not a global construct, it is part of the syntax of async functions. If a handler written as a plain function or arrow contains await, the script will not even start: the error happens at parse time, not on click.

There is exactly one exception, top level await in ES modules, and it does not apply to event handlers, because a handler body is always a function body.

A practical example: loading data on click

html
<button id="load">Load data</button> <div id="result"></div>
javascript
const btn = document.querySelector('#load'); const result = document.querySelector('#result'); btn.addEventListener('click', async () => { result.textContent = 'Loading...'; try { const res = await fetch('https://api.example.com/posts/1'); const post = await res.json(); result.textContent = post.title; } catch (err) { result.textContent = 'Failed to load'; } });

What happens here:

  • the click starts the async handler;
  • await fetch(...) pauses execution inside that function;
  • once the data arrives it goes into the DOM;
  • the interface stays responsive, because JavaScript remains asynchronous and the event loop is free.

Moving the logic into a separate async function

Sometimes it is cleaner to keep the logic away from the subscription:

javascript
async function handleClick() { const data = await fetchData(); render(data); } button.addEventListener('click', handleClick);

This behaves identically: addEventListener simply receives a function reference, and the fact that it is async is an implementation detail. This style is also easier to test and reuse.

What the browser does with the handler's promise

This is the part interviewers are really asking about:

  • Nothing awaits the handler. addEventListener does not know there is an await inside. Code after the handler call runs immediately and the returned promise is simply discarded.

  • You must catch errors yourself. An unhandled rejection ends up in unhandledrejection and can easily go unnoticed:

    javascript
    btn.addEventListener('click', async () => { try { await riskyOperation(); } catch (e) { console.error('Error:', e); } });
  • preventDefault() and stopPropagation() take effect instantly, but only while the handler is still running synchronously. After the first await the browser has finished dispatching the event and the default action can no longer be cancelled, so call them at the very top.

  • Double clicks. While an await is pending the user can click again and start a second request. The fix is simple: disable the button or keep an isLoading flag.

Summary table

QuestionAnswer
Can you use await in a handler?Yes, if the handler is async
Can you write await without async?No, it is a SyntaxError
Does it block the interface?No, the event loop stays free
Do you have to catch errors?Yes, with try/catch
Can the async function live separately?Yes, and it is the cleaner style
Does the browser wait for the handler to finish?No, the returned promise is ignored

A simple rule: await inside a handler is fine, as long as the handler is async and the errors are handled.

Common mistakes

  • Forgetting async on the handler. The script fails with a SyntaxError before the first click, and the page ends up with no handlers at all.
  • Calling preventDefault() after an await. The form has already been submitted and the link has already navigated: there is nothing left to cancel.
  • Assuming the browser waits for the handler. It does not, which is why asynchronous work inside beforeunload never gets the chance to finish.
  • Not handling errors. A rejected promise from a handler does not stop the program and often disappears silently, leaving the interface stuck on "Loading...".
  • Not guarding against repeated clicks. Several quick clicks fire several parallel requests, and the DOM ends up showing whichever finished last rather than whichever was clicked last.
  • Marking a handler async for no reason. With no asynchronous work inside, async only adds a microtask and makes the code harder to read.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.