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
awaitmakes 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
clickhandler - 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:#
- Open a blank window synchronously (retain gesture)
- Asynchronously request the URL
- On success → redirect the blank window to the target URL
- 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.