DEV Community

loading...
Cover image for Sure you want to leave?—browser beforeunload event
Google Web Dev

Sure you want to leave?—browser beforeunload event

samthor profile image Sam Thorogood Updated on ・5 min read

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
  • … and, maybe you can use sendBeacon rather than prompting at all!

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


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:

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

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, 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:

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

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. 👷‍♀️


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.

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);
}
Enter fullscreen mode Exit fullscreen mode

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:

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

  const cleanup = () => {
    pendingOps.delete(promise);
    busyspinner.hidden = (pendingOps.size === 0);
  };
  promise.then(cleanup).catch(cleanup);
}
Enter fullscreen mode Exit fullscreen mode

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

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:

// 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));
Enter fullscreen mode Exit fullscreen mode

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

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);
});
Enter fullscreen mode Exit fullscreen mode

And voilà! 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!".


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.

window.addEventListener('beforeunload', () => {
  const data = 'page-closed';
  navigator.sendBeacon('/analytics', data);
});
Enter fullscreen mode Exit fullscreen mode

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:

if (!navigator.sendBeacon) {
  navigator.sendBeacon = (url, data) =>
      window.fetch(url, {method: 'POST', body: data, credentials: 'include'}).
}
Enter fullscreen mode Exit fullscreen mode

⚠️ 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, 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. 😂👍


I hope you enjoyed this episode of The Standard and the slightly longer explanation!

Do you have questions? Please leave comments below, or contact me on Twitter. I'm also eager to hear your suggestions or improvements. 🕵️

Discussion (32)

pic
Editor guide
Collapse
yerycs profile image
yerycs • Edited

Thanks for good post.
I have one error, I want your help.

beforeunload function does not work if there is no user interaction.
It causes following error:

Blocked attempt to show a 'beforeunload' confirmation panel for a frame that never had a user gesture since its load.

How to show warning message even no user interaction?

Hoping your help.
Thanks

Collapse
samthor profile image
Sam Thorogood Author

Good observation, although I actually mentioned this in the article:

Showing a prompt isn't guaranteed. Just like playing audio on the web, browsers can ignore your request if a user hasn't interacted with your page.

This makes sense—a page that I've put into a background tab and then later closed shouldn't be able to get my attention!

Collapse
yerycs profile image
yerycs

Thanks for your reply.

Little trick.
So, is it impossible to show warning message if there is no user interaction?

Thread Thread
samthor profile image
Sam Thorogood Author

There's no harm in trying (a browser might decide that the page is allowed to—perhaps if your user has visited you a lot before) but it's unlikely to work. As per my post, maybe consider using sendBeacon if there's some information you want to exfiltrate from the page before it closes...

Even calling alert() is often disallowed in background pages (or at least delayed until the page gets focus again).

Thread Thread
yerycs profile image
yerycs

Let us assume.

I have passed complex page which has decades tests.
After passing this page, I get into the test result page.

If I click back button without any interaction in browser, then it goes to test page without confirmation, and I lost test result and should pass complex test again.

I hope it can be resolved.
Any Idea?

Thread Thread
samthor profile image
Sam Thorogood Author

Yes, that will go "back", but there are lots of other ways to store state (like your test results) that don't rely on the user keeping a page open.

e.g., indexdb, local storage, building a SPA where 'back' is handled by your code, etc ...

Thread Thread
yerycs profile image
yerycs

Understand.

Thanks for your kindly help.
Hoping your good post. :)

Collapse
yerycs profile image
yerycs

Do you think it is impossible to implement warning message without user interaction?

Thread Thread
samthor profile image
Sam Thorogood Author • Edited

Yes, I think it's impossible. Allowing an annoying popup to be generated without the user ever using the page is against what browsers are aiming for.

Thread Thread
yerycs profile image
yerycs

Can you explain about "against what browser are aiming for"?
Sorry for annoying. :)

Collapse
maxart2501 profile image
Massimo Artizzu • Edited

I've always found those notifications annoying but eh, they actually saved me a couple of times.

What's way worse in my opinions is showing those "don't miss this other content!" modals when the mouse cursor leaves the boundaries of the page. They should be outright banned! 😩

To stay on topic, I think sendBeacon is a great thing - too bad IE and mobile Safari don't support it 🙁

Collapse
oathkeeper profile image
Divyesh Parmar

How to ensure to know if a user has clicked on cancel on that alert? and Revert the action/API call that I was going to if user had proceeded to leave.

Collapse
samthor profile image
Sam Thorogood Author

Inside your beforeunload handler, you can add code in a setTimeout—probably even with a timeout of zero. That code will only run if the user allowed the page to stay open.

Collapse
oathkeeper profile image
Divyesh Parmar

Ok so we are basically making it async?

Thread Thread
samthor profile image
Sam Thorogood Author

Yeah, because your code otherwise won't have a chance to run.

I'd probably put a longer delay than zero on it, some browsers might let code execute even if the page is closing. I admit this behavior is a bit undefined.

Collapse
karapapas profile image
karapapas

Hello! Great article on beforeunload.

I have a question though, is it possible to prevent page unload and stay on the page without prompting the user? Or, if that is not possible, to at least customize the confirmation dialog?

Thank you for your time!!

Collapse
samthor profile image
Sam Thorogood Author

No, that's the whole point. You could write something like "CLOSING THIS PAGE WILL EXPLODE YOUR COMPUTER", and browsers have decided that you should not be able to do that.

Collapse
tux0r profile image
tux0r

Yes, please annoy a user who wants to leave...

Collapse
samthor profile image
Sam Thorogood Author

This feature exists on the web whether I write about it or not! :)

Hopefully this post has given some examples of when beforeunload is appropriate to use, and detailed a technical approach so that folks don't just generate warnings all the time. And you might also save your users' data—if a network request is still pending while a page is being closed, beforeunload can be useful to ensure that it does complete successfully.

Collapse
sleepyfran profile image
Fran González

This is a super useful feature that saved me countless times in pages in which I'd lose all my progress if they didn't implement it. Of course this can be used in bad situations, just like many other features, but that doesn't mean that it can't be helpful sometimes too.

Collapse
amsharma9 profile image
Amal • Edited

I would like to know if I can differentiate between beforeunload happening when closing window and moving away from page etc. I need to logout the user when s/he closes the window but NOT when s/he is moving to another page etc.

I am using sendBeacon right now as ajax didn't work.

Collapse
samthor profile image
Sam Thorogood Author

Moving to another page can be detected by watching focus or the Page Visibility API. The page is still open in that case, just hidden or not being used by the user.

Collapse
sashirocks1 profile image
Sashi • Edited

I use the following piece of code in 'beforeunload' to delete a user session during logout, but this fails to clear the user session on a firefox browser, can you please help out here
var req = new XMLHttpRequest();
req.open('DELETE', URI, true);
req.setRequestHeader("TOKEN", $rootScope._TOKEN);
req.send(null);

Collapse
samthor profile image
Sam Thorogood Author

Don't send an XMLHttpRequest—use sendBeacon, like I mentioned in the article.

Collapse
kenny08gt profile image
Alan Hurtarte

Thanks! this is very helpful, I wish it had an option to make an action when the user actually agree to leave, like delete localstorage.

Collapse
samthor profile image
Sam Thorogood Author

You could do the action and revert it if your JS runs again.

Collapse
jalson1982 profile image
Jalson1982

Any idea how to perform graphql mutation with sendBeacon?

Collapse
samthor profile image
Sam Thorogood Author

Well, you send off a POST request with sendBeacon. You'd tell your server (on some HTTP handler) to do something, but you wouldn't be able to see whether the result was successful.

Collapse
jalson1982 profile image
Jalson1982

The issue with this I can not set headers or on some way control it. I am using synchronous XMLHttp request, it runs on all browsers but slows down a little navigation and it is not also the most happier solution. I do not need to see the result I just need to save analytics data to db on browser close.

Collapse
pinguinosod profile image
David Cautin

Everything works fine, except when I try to create UI tests, seems like firefox doesn't trigger beforeunload when it is being controlled by an automated software.

Collapse
samthor profile image
Sam Thorogood Author

Unfortunately, since the browser is entirely allowed to decide whether to show this message at all (e.g. if a tab has never had focus), it might not be something you can really test in an automated fashion. 😔

Collapse
rudyhadoux profile image
rudyhadoux

Hi,

For Angular, @HostListener( 'window:beforeunload' ) seems not working for every browsers...