Sure you want to leave?—browser beforeunload event
Tags: beforeunload, html, javascript
Published: 2018-09-17 11:36:25

---
title: Sure you want to leave?—browser beforeunload event
published: true
description: How and why should you use the `beforeunload` event to prevent users from closing a webpage? Watch the video and learn more 📺🤔
tags: beforeunload, html, javascript
cover_image: https://thepracticaldev.s3.amazonaws.com/i/lolipnkngs1rndjc0x6u.png
---

{% youtube QQukWZcIptM %}

In the video, I explain a bit about the `beforeunload` event—which lets you prompt or warn your user that they're about to leave your page. If misused, this can be frustrating for your users—why would you use it? 💁‍♂️ℹ️

✅ Your user is part way through completing a form, e.g., a purchase
✅ There's a network POST that's in-flight, e.g., saving a preference
✅ Your user is writing a blog post or a comment and it'll be lost
🤷 A video or music will stop playing
⛔ Your user hasn't finished reading an article
⛔ There's an unread email inside an email client
⛔ There's a Time Sensitive Offer! Buy Now! 🙄💸

## Important To Remember

Before we get into the code, what is the **tl;dr** from my video? 📺👨‍🏫

* use the `beforeunload` event to warn a user they're going to close your page, but only when it's important
* a `Set` of `Promise` objects can be useful to control `beforeunload`
* &hellip; and, maybe you can use `sendBeacon` rather than prompting at all!

If you'd like to learn more, read on! ⬇️📖

<hr />

## Unload Basics

If you want to prompt or warn your user that they're going to close your page, you need to add code that sets `.returnValue` on a `beforeunload` event:

```js
window.addEventListener('beforeunload', (event) => {
  event.returnValue = `Are you sure you want to leave?`;
});
```

There's two things to remember.

1. Most modern browsers (Chrome 51+, Safari 9.1+ etc) will ignore what you say and just present a generic message. This prevents webpage authors from writing egregious messages, e.g., "Closing this tab will make your computer EXPLODE! 💣".

2. Showing a prompt isn't guaranteed. Just like [playing audio on the web](https://www.youtube.com/watch?v=9JnoKSwh1HU), browsers can ignore your request if a user hasn't interacted with your page. As a user, imagine opening and closing a tab that you never switch to—the background tab should _not_ be able to prompt you that it's closing.

## Optionally Show

You can add a simple condition to control whether to prompt your user by checking something within the event handler. This is fairly basic good practice, and could work well if you're just trying to warn a user that they've not finished filling out a single static form. For example:

```js
let formChanged = false;
myForm.addEventListener('change', () => formChanged = true);
window.addEventListener('beforeunload', (event) => {
  if (formChanged) {
    event.returnValue = 'You have unfinished changes!';
  }
});
```

But if your webpage or webapp is reasonably complex, these kinds of checks can get unwieldy. Sure, you can add more and more checks, but a good abstraction layer can help you and have other benefits—which I'll get to later. 👷‍♀️

<hr />

## Promises

So, let's build an abstraction layer around the `Promise` object, which represents the _future result_ of work- like a response from a network `fetch()`.

The traditional way folks are taught promises is to think of them as a single operation, perhaps requiring several steps- fetch from the server, update the DOM, save to a database. However, by sharing the `Promise`, other code can leverage it to watch when it's finished.

### Pending Work

Here's an example of keeping track of pending work. By calling `addToPendingWork` with a `Promise`—for example, one returned from `fetch()`—we'll control whether to warn the user that they're going to unload your page. 

```js
const pendingOps = new Set();

window.addEventListener('beforeunload', (event) => {
  if (pendingOps.size) {
    event.returnValue = 'There is pending work. Sure you want to leave?';
  }
});

function addToPendingWork(promise) {
  pendingOps.add(promise);
  const cleanup = () => pendingOps.delete(promise);
  promise.then(cleanup).catch(cleanup);
}
```

Now, all you need to do is call `addToPendingWork(p)` on a promise, maybe one returned from `fetch()`. This works well for network operations and such- they naturally return a `Promise` because you're blocked on something outside the webpage's control.

### Busy Spinner

As I talked about in the video above 📺🔝, we can also use the set of pending work to control a busy spinner. This is a pretty simple extension to the `addToPendingWork` function:

```js
function addToPendingWork(promise) {
  busyspinner.hidden = false;
  pendingOps.add(promise);

  const cleanup = () => {
    pendingOps.delete(promise);
    busyspinner.hidden = (pendingOps.size === 0);
  };
  promise.then(cleanup).catch(cleanup);
}
```

When a new `Promise` is added, we show the spinner (by setting its `.hidden` property to `false`). And when any promise finishes, we detect if there's no more work at all— and hide the spinner if `pendingOps` is empty.

![Simple checkboxes and busy spinner](https://thepracticaldev.s3.amazonaws.com/i/lk82obf0jlovbxee6zfc.gif)

I'm not a UX designer, so building a visually appealing busy spinner is a UX exercise left for the reader! 👩‍🎨

### Pending Forms

But what about for the example above- a pending form? There's two options here. You could add a _second_ `beforeunload` handler, just like the one at the top of this article: a simple boolean check.

But if you're interested in using the `Promise` mechanic even for a form, it turns out we can promisify the _concept_ of a user completing a form. There's two parts to this idea.

First, we create our own `Promise` and add it to our pending work it when the user starts typing something:

```js
// create a Promise and send it when the user starts typing
let resolvePendingFormPromise;
const pendingFormPromise =
    new Promise((resolve) => resolvePendingFormPromise = resolve);

// when the user types in the form, add the promise to pending work
myForm.addEventListener('change', () => addToPendingWork(pendingFormPromise));
```

Then, when the form is submitted (potentially via `fetch()`), we can "resolve" that original promise with the result of the network operation:

```js
myForm.addEventListener('submit', (event) => {
  event.preventDefault();  // submitting via fetch()

  const p = window.fetch('/submit', ...).then((r) => r.json());
  p.then((out) => { /* update the page with JSON output */ });

  // resolve our "pending work" when the fetch() is done
  resolvePendingFormPromise(p);
});
```

And [voilà](https://google.com/search?q=define%3Avoilà)! If the user has typed into the form, we can block the page from unloading, using the same pending work idiom as before. Of course, your busy spinner probably shouldn't say "Saving!".

<hr />

## Send a Beacon

I've covered a lot on pending work, listening to the completion of promise from a `fetch()`. But, as I mentioned in the video, you might not always need to prompt the user at all.

If you're making a network request which has no useful result- you're just sending it to a server, and you don't care about the result- you can use the modern browser call `navigator.sendBeacon()`. It literally has _no_ return value, so you _can't_ wait for its result (whether that be success _or_ failure). But, it's explicitly designed to run even after a page is closed.

```js
window.addEventListener('beforeunload', () => {
  const data = 'page-closed';
  navigator.sendBeacon('/analytics', data);
});
```

Of course, you don't have to use `sendBeacon` only in `beforeunload`—you can use it _before_ the page is closed, and then you might not have to implement a `beforeunload` handler at all, because you don't have a pending `Promise` to wait for!

### Polyfill

If your browser doesn't support `sendBeacon`, it's almost exactly equal to sending a POST request via `fetch()`. You could fallback using code like this:

```js
if (!navigator.sendBeacon) {
  navigator.sendBeacon = (url, data) =>
      window.fetch(url, {method: 'POST', body: data, credentials: 'include'}).
}
```

⚠️ It's even worth doing this if you're trying to make network requests in `beforeunload`, as some browsers will still succeed a `fetch()` even though the spec doesn't guarantee it.

### Emoji Example

I use `navigator.sendBeacon()` to record when you select an emoji on [Emojityper](https://emojityper.com), to generate the 'trending' 📈 list and emoji popularity 🔥. It's suitable there as I don't need to wait for a response, and the request can go out even as you're closing the page. 😂👍

<hr />

I hope you enjoyed this episode of [The Standard](https://www.youtube.com/playlist?list=PLNYkxOF6rcIB1V2i_qfRtDMcY6YZK1lkt) and the slightly longer explanation!

Do you have questions? Please leave comments below, or contact me on [Twitter](https://twitter.com/samthor). I'm also eager to hear your suggestions or improvements. 🕵️