banner
yyh

Hi, I'm yyh

github
x
email

Best Practices for Safely Opening New Tabs When Asynchronously Fetching URLs: Avoiding Popup Blockers

In many SaaS / Web products, we encounter this typical requirement:

User clicks a button → The frontend needs to call an API to get a dynamic URL → Then open it in a new tab.

It seems simple, but if you write it directly as:

const url = await fetch('/api/url')
window.open(url)

You will 100% encounter issues:

  • The browser will block the popup (because await makes it lose user gesture)
  • The newly opened page may be able to control your site through window.opener
  • A slow or failed request will give users a feeling of "no response"
  • Some browsers will directly ignore window.open

These are not explicit errors, but rather very subtle engineering pitfalls.

This article introduces a universal security solution that has been validated through practice:
How to avoid popup blocking while still maintaining noopener,noreferrer security isolation in the case of asynchronously obtaining a URL.


1. Why is it blocked by the browser?#

Browsers only allow new windows to be opened in synchronous user gesture (user gesture) events, such as:

  • Synchronous code within a click handler
  • Synchronous code in onTouchStart

Once you have:

await ...
setTimeout(...)
Promise.then(...)

The browser considers this popup not triggered by the user → it will be blocked.

This is the ultimate reason for "no response after clicking the button."


2. Universal solution: placeholder tab#

To retain the user gesture while waiting for the asynchronous URL, we need a trick:

Steps:#

  1. Open a blank window synchronously (retain gesture)
  2. Asynchronously request the URL
  3. On success → redirect the blank window to the target URL
  4. On failure → close the blank window, leaving no junk tabs

Code example (universal minimal implementation)#

const handleClick = async () => {
  // 1. Synchronously open a placeholder window
  const placeholder = window.open('', '_blank', 'noopener,noreferrer')

  try {
    // 2. Asynchronously obtain the URL (example)
    const res = await fetch('/api/target-url')
    const { url } = await res.json()

    // 3. Redirect if the URL is valid
    if (url) {
      placeholder.location.href = url
      return
    }
  } catch (err) {
    console.error('Failed to load url', err)
  }

  // 4. On failure, close the placeholder window
  placeholder?.close()
}

(To prevent some browsers from ignoring the feature string, you can execute placeholder && (placeholder.opener = null) after obtaining the window; if window.open is blocked and returns null, you should directly prompt the user or callback the error.)

Why do it this way?#

  • Not blocked: The placeholder window is opened in synchronous code
  • Sufficiently secure: Always use noopener,noreferrer
  • Good experience: No blank tab left on failure
  • Easy to maintain: All logic related to opening new tabs is centralized in one function

3. The significance of noopener / noreferrer#

By default:

window.open(url)

The opened page can access:

window.opener

Thus reverse controlling your page:

  • Changing your URL (tabnabbing attack)
  • Injecting malicious scripts
  • Simulating login redirects to a spoofed site

So we must always write:

window.open(url, '_blank', 'noopener,noreferrer')

Where:

  • noopener: Prevents the new page from obtaining window.opener
  • noreferrer: Does not pass the referrer, further isolating the context

This is a security baseline that any frontend code opening external links should adhere to.


4. User experience issues with request delays#

If the API response is slow, users may mistakenly think the click was ineffective.

The placeholder window has a natural advantage:

  • The browser has already opened the tab
  • Users see that it is loading
  • Once the URL arrives, it automatically redirects, not missing the gesture opportunity
  • On failure, it closes, leaving no junk tabs

This is a simple yet very effective UX improvement.


5. What scenarios does this pattern apply to?#

As long as it meets the business requirement of "needing to asynchronously obtain a URL after clicking," it can be used:

  • Opening third-party management backends (Billing, Portal, Dashboard)
  • Redirecting to temporary URLs that require signing (S3, R2, GCS, MinIO, COS)
  • OAuth, SSO, temporary authorization pages
  • Exporting/downloading resources that need to be generated asynchronously
  • External link redirection with high security requirements
  • Dynamically generated payment/receipt/subscription pages

This pattern is not limited to frameworks:
React, Vue, Svelte, Next.js, plain HTML can all use it.


6. Conclusion#

If a link needs to be asynchronously obtained through an API
Then the "placeholder window + asynchronous filling" pattern must be used
To avoid popup blocking and ensure security isolation.

Key points:

  • Synchronous window.open → Retain user gesture
  • Always use noopener,noreferrer → Prevent reverse control
  • Close the window on failure → Avoid meaningless tabs
  • Redirect only after the request is completed → Balance experience and security

This is a technique that is often overlooked in engineering practice but is very important.
If your application involves external link redirection, temporary URLs, OAuth, Billing, or downloading files, it can significantly improve user experience and security.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.