skip to content

In the browser, what does navigator.permissions.query({ name: 'geolocation' }) actually tell you, what values can the resulting PermissionStatus.state hold, and what does the call deliberately not do?

level: middleimportance: should knowfreq 45%

answer

  1. reads state, never prompts
  2. three strings, not two
  3. 'prompt' means undecided
  4. PermissionStatus fires change
  5. no request() method exists

basics

~20 s

It reads the current permission state without any user interface, resolving to a PermissionStatus whose state is 'granted', 'denied' or 'prompt'. Querying never asks the user; only the feature's own API, such as getCurrentPosition(), triggers a prompt.

solid answer

~50 s

`navigator.permissions.query()` is a read of stored permission state. It returns a promise for a `PermissionStatus` whose `state` is one of `'granted'`, `'denied'` or `'prompt'` — the last meaning the user has not decided yet, so the feature would show a prompt if invoked. Crucially the query itself shows nothing: it is safe to call on page load, which is what makes it useful for deciding whether to render a "share my location" button, a "you blocked this, here is how to re-enable it" hint, or nothing at all. `PermissionStatus` is an `EventTarget` and fires a `change` event, so you can react when the user flips the setting in site settings while the page is open. Two gotchas: the set of supported permission names differs by browser and an unknown name rejects with a `TypeError`, and there is no `navigator.permissions.request()` — prompting always goes through the feature's own API.

code

javascript · 17 lines
javascript
async function watchGeolocationPermission(render) {
  if (!navigator.permissions) {
    render('unknown');
    return null;
  }
  try {
    const status = await navigator.permissions.query({ name: 'geolocation' });
    render(status.state);
    status.addEventListener('change', () => render(status.state));
    return status;
  } catch {
    render('unknown');
    return null;
  }
}

watchGeolocationPermission((state) => console.log('geolocation:', state));

go deeper

for a junior

Recall that the Permissions API lets you read whether a feature is allowed without bothering the user, and that the three answers are granted, denied and prompt. Say that asking still happens through the feature's own call.

for a middle

Explain the read-versus-ask split, the change event on PermissionStatus, and why an unrecognised name rejects with a TypeError. Distinguish the 'prompt' string here from Notification.permission's 'default'.

for a senior

Demonstrate using the state to shape the interface: suppress a doomed call when denied, surface a site-settings recovery hint, and log the state next to failures so 'user refused' is never confused with 'API broken'.

for a principal

Own the policy: which features may prompt at all, what the degraded experience is for each denial, and how permission outcomes are instrumented so the product can see grant rates instead of guessing at them.

## What the Permissions API is for Before the Permissions API existed there was no uniform way to ask "has the user already granted this?" Each feature had its own convention or none at all, so the only way to find out was to call the feature and see whether a prompt appeared — which is exactly the user-hostile thing you wanted to avoid. `navigator.permissions.query()` separates *reading* the decision from *asking* for it. ```js const status = await navigator.permissions.query({ name: 'geolocation' }); console.log(status.state); // 'granted' | 'denied' | 'prompt' ``` ## The three states - **`'granted'`** — the user has allowed it for this origin. Calling the feature should proceed without UI. - **`'denied'`** — the user has refused, or the browser has blocked it (site settings, an enterprise policy, an embargo after repeated dismissals, or a Permissions Policy that disables the feature for this document). Calling the feature will fail immediately with no prompt. - **`'prompt'`** — no stored decision. Calling the feature will show the browser's permission UI. A common mix-up is with the older `Notification.permission` property, whose third value is `'default'`, not `'prompt'`. Both describe "undecided", but the strings differ because the Notification API predates the Permissions API. ## What query() does not do It never displays UI, and it never changes state. That is the whole point — the call is side-effect-free, so you can run it during startup for several features at once and shape the interface accordingly. It also does not guarantee that the feature will work. `'granted'` means the user's decision is recorded; the call can still fail because the hardware is absent, because the document is not a secure context, because a Permissions Policy disabled the feature, or because the API additionally requires a user gesture. Treat the query result as *guidance for the UI* and still handle rejection at the call site. And there is no companion `request()` method. It was proposed but never shipped in browsers, so the only way to raise a prompt is the feature's own entry point: `navigator.geolocation.getCurrentPosition()`, `Notification.requestPermission()`, `navigator.mediaDevices.getUserMedia()`, and so on. A candidate who reaches for `navigator.permissions.request()` is describing an API that does not exist. ## Reacting to changes `PermissionStatus` extends `EventTarget` and fires `change` when the stored decision moves. This matters for a real behaviour: a user who is blocked opens the browser's site-settings panel, flips the switch to allow, and returns to your still-open tab. Without a listener your page keeps showing the blocked state until a reload. ```js const status = await navigator.permissions.query({ name: 'geolocation' }); render(status.state); status.addEventListener('change', () => render(status.state)); ``` Keep a reference to the `PermissionStatus` object; if it is garbage collected the listener goes with it. ## Names and browser differences The permission name is a string from a registry that browsers implement unevenly. `'geolocation'`, `'notifications'`, `'camera'` and `'microphone'` are broadly available; names such as `'clipboard-read'` and `'clipboard-write'` are Chromium-specific, and `'push'` in Chromium requires the extra dictionary member `userVisibleOnly: true` or it rejects. Query rejects with a `TypeError` when it does not recognise the name, so wrap it: ```js async function permissionState(name) { try { const status = await navigator.permissions.query({ name }); return status.state; } catch { return 'unknown'; // name unsupported in this browser } } ``` Even `navigator.permissions` itself should be feature-detected before use, and the whole API is restricted to secure contexts. ## Using it well The strongest use is deciding *whether to ask at all*. If the state is `'denied'`, do not call the feature — it will fail silently from the user's point of view and you will have burned a click. Show a recovery path instead, explaining that the setting lives in the browser's own site settings, because no script can reset a denial. If the state is `'prompt'`, keep the prompt behind an explicit user action so the request arrives with visible context. If it is `'granted'`, go straight to the feature with no interstitial of your own. A second use is diagnostics. Logging the state alongside a feature failure separates "the user said no" from "the API broke", which are very different bug reports.

  • You query and get 'granted', then the call still fails. Name two reasons.
    A stored grant is only one of several gates. The document may not be a secure context, a Permissions Policy may disable the feature for this frame, the required hardware may be missing or in use, or the API may additionally demand transient user activation. Treat the query as UI guidance and always handle the rejection at the call site.
  • Why keep a reference to the PermissionStatus object rather than discarding it after reading state?
    `state` is a live snapshot on a `PermissionStatus`, which is an `EventTarget` firing `change`. Holding the object and listening lets the page update when the user flips the setting in the browser's own site settings mid-session. Drop the reference and the object — with the listener attached to it — becomes collectable.
  • How would you handle a browser that does not recognise the permission name you pass?
    `query()` rejects with a `TypeError` for an unknown name, so wrap it and fall back to an 'unknown' state that means "proceed as if undecided". Never let a rejected query break rendering: unsupported names are common — clipboard names are Chromium-specific, and Chromium's 'push' also requires `userVisibleOnly: true`.

saying these in an interview costs you the question

  • Says query() shows the permission prompt
  • Gives the states as granted, denied and default
  • Claims navigator.permissions.request() triggers the ask
  • Assumes granted guarantees the API call succeeds
  • Expects every permission name to work in every browser

context