pushManager.subscribe() Fails With NotAllowedError
A NotAllowedError from pushManager.subscribe() is the browser refusing to create a subscription — almost always a permission, gesture, or context problem rather than a network or server fault.
Quick answer
pushManager.subscribe() throws NotAllowedError when the browser will not grant a push subscription. The common causes are: notification permission is not granted (it is default or denied), the call did not happen in response to a user gesture (required by Safari and increasingly by Chromium), the page is in an insecure context (not HTTPS or localhost), you passed userVisibleOnly: false without an allowance the browser grants, or the applicationServerKey is malformed or differs from an existing subscription. Request permission on a click, serve over HTTPS, and always pass userVisibleOnly: true with a correctly decoded VAPID key.
Why this happens
pushManager.subscribe() is gated by the Notifications permission and by the platform’s user-activation rules. The browser maps several distinct refusals onto the same NotAllowedError name, which is why the message alone rarely tells you the cause. The most frequent trigger is calling subscribe() while Notification.permission is still default (the user never answered) or denied (they said no) — the Push API will not silently create a subscription without an active grant. WebKit additionally requires that both the permission request and the subscribe call originate from a genuine user gesture, so calling them on page load or inside an async chain that has lost the activation throws.
Context matters too. Service workers and the Push API only function on secure origins, so a page served over plain HTTP (anything other than localhost) cannot subscribe. And userVisibleOnly: false — a request to send silent pushes — is refused by every major browser unless your origin has a specific allowance, surfacing as NotAllowedError. Finally, the applicationServerKey must be a Uint8Array derived from your base64url VAPID public key; a string, a wrong-length array, or a key that conflicts with an existing subscription is rejected. The full registration lifecycle is covered in service worker registration patterns and the Core Protocols & Browser Implementation overview.
The reason a single error name covers so many causes is historical: the Push API reuses the DOM NotAllowedError (a DOMException) to mean “the user agent declined this operation for a policy or permission reason.” That is intentionally broad. A denied permission, a missing user gesture, and a silent-push request are all, from the browser’s perspective, the same kind of refusal — the platform decided not to grant the capability. This is why reading err.message rather than err.name is essential: Chromium and Safari include human-readable detail in the message (“Registration failed - permission denied”, “Subscription failed - no active Service Worker”) that tells you which refusal you hit, even though the name is uniformly NotAllowedError.
User activation deserves special attention because it is the cause developers most often miss. Browsers track a transient “user activation” flag that is set by a real input event (click, tap, key press) and consumed or expired shortly after. WebKit requires this flag to be present when requestPermission() runs, and the flag does not survive arbitrary await boundaries. A handler that awaits a network call, a serviceWorker.register(), or even a microtask chain before requesting permission can find the activation gone, producing a NotAllowedError that is impossible to reproduce when stepping through the debugger — because pausing restores focus and re-grants activation. Request permission synchronously at the top of the click handler, then do async work afterward.
NotAllowedError versus the other subscribe exceptions
It helps to know what NotAllowedError is not, because misreading it sends you debugging the wrong layer. If subscribe() throws AbortError, the push service rejected the registration (often a transient backend issue or a bad endpoint) — that is a network/service problem, not a permission one. If it throws InvalidStateError, there is no active service worker controlling the page, so you must await navigator.serviceWorker.ready first. If it throws InvalidAccessError or a TypeError about the key, the applicationServerKey is the wrong type or length. Only NotAllowedError points at permission, gesture, context, or the userVisibleOnly policy. Branching on err.name lets you route each failure to the right fix instead of treating every subscribe failure as a permission denial.
A frequently overlooked detail is that permission and subscription are separate states that can drift apart. A user can grant notification permission, get subscribed, then clear site data — which revokes the subscription but may leave the permission cached, or vice versa. So before subscribing, do not assume that a previous granted permission implies a live subscription, and do not assume an existing subscription implies current permission. Check both Notification.permission and pushManager.getSubscription() and reconcile them; a subscribe call made on a stale assumption is a common source of an unexpected NotAllowedError in returning-user flows.
Correct subscribe flow
The flow below requests permission on a real click, verifies the grant, serves the secure-context requirement implicitly (HTTPS), and decodes the VAPID key correctly before subscribing.
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const raw = atob(base64);
return Uint8Array.from([...raw].map((c) => c.charCodeAt(0)));
}
// MUST be called from a click/tap handler, not on page load
async function subscribeToPush(vapidPublicKey) {
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
throw new Error('Push not supported in this browser');
}
// 1. Permission must be granted BEFORE subscribe(); request it on the gesture
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
// 'denied' or 'default' both cause NotAllowedError on subscribe()
throw new Error(`Notification permission: ${permission}`);
}
const registration = await navigator.serviceWorker.ready;
try {
return await registration.pushManager.subscribe({
userVisibleOnly: true, // false triggers NotAllowedError without an allowance
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey),
});
} catch (err) {
if (err.name === 'NotAllowedError') {
// Permission revoked mid-flow, no user gesture, or key/context problem
console.error('subscribe blocked:', err.message);
}
throw err;
}
}
On the server the public key must come from the environment — never hardcode the VAPID public key server-side. The key the client decodes must be the same one your server signs with; see VAPID key generation and rotation.
// Expose the public key to the client from env, not a literal
app.get('/vapid-public-key', (req, res) => {
res.send(process.env.VAPID_PUBLIC_KEY);
});
Diagnostic steps
- Log
Notification.permissionbefore subscribing. If it isdefault, you never got a grant; ifdenied, the user blocked it and you cannot re-prompt programmatically. Both causeNotAllowedError. - Confirm a user gesture. Ensure
requestPermission()andsubscribe()run synchronously inside a click handler. Anawaitbefore the permission call can consume the activation in WebKit. - Verify the secure context. Check
window.isSecureContext. If false, move to HTTPS (orlocalhostfor development) — the Push API will not work otherwise. - Inspect
userVisibleOnly. Make sure it istrue. Setting it tofalsewithout a granted allowance throws on every major browser. - Validate the
applicationServerKey. It must be aUint8Arrayof 65 bytes from your base64url VAPID public key, not a raw string. A decoding bug here is a quietNotAllowedError. - Check for an existing subscription with a different key. Call
pushManager.getSubscription(); if one exists under a differentapplicationServerKey, unsubscribe it before subscribing with the new key.
Cause-to-fix reference
| Cause | How to confirm | Fix |
|---|---|---|
Permission default (never answered) |
Notification.permission === 'default' |
Call requestPermission() on a gesture first |
Permission denied |
Notification.permission === 'denied' |
Cannot re-prompt; direct user to settings |
| No user gesture (WebKit) | Error only outside a click handler | Request permission synchronously in the handler |
| Insecure context | window.isSecureContext === false |
Serve over HTTPS or use localhost |
userVisibleOnly: false |
Flag is false in subscribe options | Set it to true |
Bad applicationServerKey |
Key is a string or wrong length | Decode base64url to a 65-byte Uint8Array |
| Key mismatch with existing sub | getSubscription() returns a sub |
unsubscribe() before re-subscribing |
Work down this table in order: permission state and user gesture account for the overwhelming majority of real-world NotAllowedError reports, and both are cheap to check before you suspect the key or context.
Gotchas and edge cases
deniedis sticky. Once a user denies notifications, you cannot re-prompt with JavaScript —requestPermission()resolves todeniedimmediately. Guide users to browser settings instead.- Losing the user gesture across
await. In Safari, awaiting something beforerequestPermission()can drop the user activation, turning a valid flow into aNotAllowedError. Request permission first. - iOS requires an installed PWA. On iOS,
subscribe()only works inside a Home-Screen PWA; in a normal Safari tab it fails regardless of permission state. - A string
applicationServerKeysilently fails or throws. It must be aUint8Array(orArrayBuffer). Passing the base64url string directly is a frequent cause. - Key mismatch on an existing subscription. A subscription is bound to the
applicationServerKeyit was created with; subscribing again with a different key requires unsubscribing first or it errors.
Related
- Service Worker Registration Patterns — registering and readying the worker that owns the
pushManager. - VAPID Key Generation & Rotation — generating the
applicationServerKeyand keeping client and server keys aligned. - Cross-Browser Notification Quirks — per-browser permission and gesture differences behind subscribe failures.