Permissions API
The Permissions API provides a unified way to check the status of browser permissions for features like camera, microphone, geolocation, notifications, and more. It allows you to query whether a permission is "granted", "denied", or "prompt" (not yet decided) before attempting to use the corresponding API.
Instead of blindly calling an API and catching permission errors, you can check the permission status first and provide a better user experience. The API also fires change events when the user grants or revokes permissions, so your app can react in real time.
| 1 | // Check notification permission |
| 2 | const result = await navigator.permissions.query({ name: "notifications" }); |
| 3 | console.log(result.state); // "granted", "denied", or "prompt" |
| 4 | |
| 5 | // React to permission changes |
| 6 | result.addEventListener("change", () => { |
| 7 | console.log("Permission changed to:", result.state); |
| 8 | }); |
| 9 | |
| 10 | // Check geolocation permission |
| 11 | const geo = await navigator.permissions.query({ name: "geolocation" }); |
| 12 | console.log("Geolocation:", geo.state); |
The query() method takes a PermissionDescriptor object with a name property specifying the permission to check. It returns a Promise that resolves to a PermissionStatus object with a state property.
| Permission Name | API | Description |
|---|---|---|
| geolocation | Geolocation API | Location access |
| notifications | Notifications API | System notifications |
| camera | MediaDevices API | Camera access |
| microphone | MediaDevices API | Microphone access |
| clipboard-read | Clipboard API | Read clipboard |
| clipboard-write | Clipboard API | Write clipboard |
| midi | Web MIDI API | MIDI device access |
| 1 | // Query a specific permission |
| 2 | async function checkPermission(name) { |
| 3 | try { |
| 4 | const status = await navigator.permissions.query({ name }); |
| 5 | return status.state; |
| 6 | } catch (err) { |
| 7 | // Permission name not supported in this browser |
| 8 | console.warn(`Permission "${name}" not supported:`, err.message); |
| 9 | return "unsupported"; |
| 10 | } |
| 11 | } |
| 12 | |
| 13 | // Check multiple permissions |
| 14 | async function checkAllPermissions() { |
| 15 | const permissions = [ |
| 16 | "geolocation", |
| 17 | "notifications", |
| 18 | "camera", |
| 19 | "microphone", |
| 20 | "clipboard-read" |
| 21 | ]; |
| 22 | |
| 23 | const results = {}; |
| 24 | for (const name of permissions) { |
| 25 | results[name] = await checkPermission(name); |
| 26 | } |
| 27 | |
| 28 | return results; |
| 29 | } |
| 30 | |
| 31 | const status = await checkAllPermissions(); |
| 32 | console.log(status); |
| 33 | // { |
| 34 | // geolocation: "granted", |
| 35 | // notifications: "prompt", |
| 36 | // camera: "denied", |
| 37 | // microphone: "prompt", |
| 38 | // clipboard-read: "unsupported" |
| 39 | // } |
The PermissionStatus object returned by query() has a state property and a changeevent. The state can be "granted" (permission given), "denied" (permission refused), or "prompt" (user hasn't decided yet).
| 1 | // PermissionStatus object |
| 2 | const status = await navigator.permissions.query({ name: "notifications" }); |
| 3 | |
| 4 | console.log(status.state); // "granted", "denied", or "prompt" |
| 5 | console.log(status.name); // "notifications" |
| 6 | |
| 7 | // Listen for permission changes |
| 8 | status.addEventListener("change", () => { |
| 9 | console.log(`Permission "${status.name}" changed to: ${status.state}`); |
| 10 | |
| 11 | switch (status.state) { |
| 12 | case "granted": |
| 13 | showUI("Notifications enabled"); |
| 14 | break; |
| 15 | case "denied": |
| 16 | showUI("Notifications blocked — enable in settings"); |
| 17 | break; |
| 18 | case "prompt": |
| 19 | showUI("Notifications not yet decided"); |
| 20 | break; |
| 21 | } |
| 22 | }); |
| 23 | |
| 24 | // Practical: adaptive UI based on permissions |
| 25 | async function setupFeatureUI() { |
| 26 | const geoStatus = await navigator.permissions.query({ name: "geolocation" }); |
| 27 | const notifStatus = await navigator.permissions.query({ name: "notifications" }); |
| 28 | |
| 29 | if (geoStatus.state === "granted") { |
| 30 | showLocationMap(); |
| 31 | } else if (geoStatus.state === "prompt") { |
| 32 | showLocationPrompt(); |
| 33 | } else { |
| 34 | showLocationDenied(); |
| 35 | } |
| 36 | |
| 37 | if (notifStatus.state === "granted") { |
| 38 | enableNotifications(); |
| 39 | } |
| 40 | } |
Checking camera and microphone permissions before calling getUserMedia()prevents the browser from showing a permission dialog when the user doesn't expect it. This creates a smoother user experience for video/audio features.
| 1 | // Check camera/mic permissions before requesting media |
| 2 | async function startCameraSafely(videoElement) { |
| 3 | // Check permissions first |
| 4 | const cameraStatus = await navigator.permissions.query({ name: "camera" }); |
| 5 | const micStatus = await navigator.permissions.query({ name: "microphone" }); |
| 6 | |
| 7 | console.log("Camera:", cameraStatus.state); |
| 8 | console.log("Microphone:", micStatus.state); |
| 9 | |
| 10 | // If already denied, don't bother asking |
| 11 | if (cameraStatus.state === "denied") { |
| 12 | showSetupInstructions("camera"); |
| 13 | return null; |
| 14 | } |
| 15 | |
| 16 | // If granted or prompt, proceed |
| 17 | try { |
| 18 | const stream = await navigator.mediaDevices.getUserMedia({ |
| 19 | video: cameraStatus.state !== "denied", |
| 20 | audio: micStatus.state !== "denied" |
| 21 | }); |
| 22 | |
| 23 | videoElement.srcObject = stream; |
| 24 | return stream; |
| 25 | } catch (err) { |
| 26 | console.error("Media access failed:", err); |
| 27 | if (err.name === "NotAllowedError") { |
| 28 | showSetupInstructions("camera/microphone"); |
| 29 | } |
| 30 | return null; |
| 31 | } |
| 32 | } |
| 33 | |
| 34 | function showSetupInstructions(device) { |
| 35 | const msg = `${device} access was denied. Please enable it in your browser settings.`; |
| 36 | alert(msg); |
| 37 | } |
| 38 | |
| 39 | // Permission-aware video call setup |
| 40 | async function setupVideoCall() { |
| 41 | const video = document.getElementById("local-video"); |
| 42 | const camera = await navigator.permissions.query({ name: "camera" }); |
| 43 | const mic = await navigator.permissions.query({ name: "microphone" }); |
| 44 | |
| 45 | // Update UI based on current permissions |
| 46 | updateMediaUI({ |
| 47 | camera: camera.state, |
| 48 | microphone: mic.state |
| 49 | }); |
| 50 | |
| 51 | // Listen for changes |
| 52 | camera.addEventListener("change", () => updateMediaUI({ camera: camera.state })); |
| 53 | mic.addEventListener("change", () => updateMediaUI({ microphone: mic.state })); |
| 54 | |
| 55 | // Start camera if possible |
| 56 | if (camera.state === "granted") { |
| 57 | await startCameraSafely(video); |
| 58 | } |
| 59 | } |
Geolocation permission can be checked before calling navigator.geolocation.getCurrentPosition(). This avoids showing the permission dialog when the user has already denied location access.
| 1 | // Geolocation with permission check |
| 2 | async function getLocation() { |
| 3 | try { |
| 4 | const status = await navigator.permissions.query({ name: "geolocation" }); |
| 5 | |
| 6 | if (status.state === "denied") { |
| 7 | console.log("Location access denied"); |
| 8 | return null; |
| 9 | } |
| 10 | |
| 11 | return new Promise((resolve, reject) => { |
| 12 | navigator.geolocation.getCurrentPosition( |
| 13 | (position) => { |
| 14 | resolve({ |
| 15 | lat: position.coords.latitude, |
| 16 | lng: position.coords.longitude, |
| 17 | accuracy: position.coords.accuracy |
| 18 | }); |
| 19 | }, |
| 20 | (error) => { |
| 21 | console.error("Geolocation error:", error.message); |
| 22 | reject(error); |
| 23 | }, |
| 24 | { |
| 25 | enableHighAccuracy: true, |
| 26 | timeout: 10000, |
| 27 | maximumAge: 60000 |
| 28 | } |
| 29 | ); |
| 30 | }); |
| 31 | } catch (err) { |
| 32 | console.error("Permission check failed:", err); |
| 33 | return null; |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | // React to geolocation permission changes |
| 38 | async function watchGeoPermission() { |
| 39 | const status = await navigator.permissions.query({ name: "geolocation" }); |
| 40 | |
| 41 | status.addEventListener("change", () => { |
| 42 | if (status.state === "granted") { |
| 43 | console.log("Location access granted — starting location tracking"); |
| 44 | startLocationTracking(); |
| 45 | } else if (status.state === "denied") { |
| 46 | console.log("Location access denied — stopping tracking"); |
| 47 | stopLocationTracking(); |
| 48 | } |
| 49 | }); |
| 50 | } |
The Permissions API is not supported in all browsers (notably absent in Safari at the time of writing). Always feature-detect and provide fallbacks. The pattern of catching errors from the underlying API works everywhere, even when the Permissions API is unavailable.
| 1 | // Progressive enhancement pattern |
| 2 | async function checkPermissionGracefully(permissionName) { |
| 3 | // Try Permissions API first |
| 4 | if ("permissions" in navigator) { |
| 5 | try { |
| 6 | const status = await navigator.permissions.query({ name: permissionName }); |
| 7 | return { |
| 8 | supported: true, |
| 9 | state: status.state, |
| 10 | onChange: (callback) => status.addEventListener("change", callback) |
| 11 | }; |
| 12 | } catch (err) { |
| 13 | // Permission name not recognized |
| 14 | } |
| 15 | } |
| 16 | |
| 17 | // Fallback: return "unknown" — caller should try the API directly |
| 18 | return { |
| 19 | supported: false, |
| 20 | state: "unknown", |
| 21 | onChange: () => {} // no-op |
| 22 | }; |
| 23 | } |
| 24 | |
| 25 | // Usage |
| 26 | const notifPerm = await checkPermissionGracefully("notifications"); |
| 27 | if (notifPerm.supported) { |
| 28 | if (notifPerm.state === "granted") { |
| 29 | enableNotifications(); |
| 30 | } |
| 31 | notifPerm.onChange(() => { |
| 32 | // Re-check and update UI |
| 33 | checkPermissionGracefully("notifications").then(p => { |
| 34 | if (p.state === "granted") enableNotifications(); |
| 35 | else disableNotifications(); |
| 36 | }); |
| 37 | }); |
| 38 | } else { |
| 39 | // Permissions API not available — try requesting directly |
| 40 | try { |
| 41 | const result = await Notification.requestPermission(); |
| 42 | if (result === "granted") enableNotifications(); |
| 43 | } catch (err) { |
| 44 | console.log("Cannot check notification permission"); |
| 45 | } |
| 46 | } |
best practice