Geolocation API
The Geolocation API lets a web page ask the device for its current position. The browser is the gatekeeper: it shows a permission prompt, decides which signals to combine (GPS, Wi-Fi, IP), and returns a coordinate or an error. The API is tiny — three methods — but the workflow around permission and accuracy deserves care.
The starting point
Everything lives on navigator.geolocation. It is undefined on insecure origins — geolocation only works over https: (and localhost).
if ("geolocation" in navigator) {
// safe to use navigator.geolocation
} else {
// unsupported, no fallback in the browser itself
}getCurrentPosition — a one-shot reading
The most common call asks for the position once. It is callback-based, not promise-based, so wrap it if you want await.
navigator.geolocation.getCurrentPosition(
(pos) => {
const { latitude, longitude, accuracy } = pos.coords;
console.log(`${latitude}, ${longitude} (±${accuracy}m)`);
},
(err) => {
console.warn("geolocation failed:", err.code, err.message);
},
{
enableHighAccuracy: false,
timeout: 10_000,
maximumAge: 60_000,
}
);The position object
The success callback receives a GeolocationPosition with two fields:
timestamp— when the reading was taken.coords— aGeolocationCoordinateswithlatitude,longitude,accuracy(in metres), and, where available,altitude,altitudeAccuracy,heading,speed.
On a laptop without GPS, you typically get accuracy of a few hundred metres derived from Wi-Fi / IP. On a phone with GPS enabled, sub-10 m is normal outdoors.
The error object
GeolocationPositionError codes
// err.code === 1 → PERMISSION_DENIED (user said no, or origin is blocked) // err.code === 2 → POSITION_UNAVAILABLE (no signal, hardware error) // err.code === 3 → TIMEOUT (took longer than options.timeout)
watchPosition — a live stream
watchPosition calls your success callback every time the position changes by a meaningful amount. It returns a numeric id you pass to clearWatch to stop.
const id = navigator.geolocation.watchPosition(
(pos) => updateMap(pos.coords),
(err) => console.warn(err),
{ enableHighAccuracy: true }
);
// later, when leaving the page or feature:
navigator.geolocation.clearWatch(id);Promisifying the API
The callback shape is awkward in modern code. A 4-line wrapper turns it into a promise.
function getPosition(options) {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, options);
});
}
try {
const pos = await getPosition({ timeout: 8000 });
console.log(pos.coords.latitude, pos.coords.longitude);
} catch (err) {
console.warn("could not get position:", err.message);
}The options object in detail
enableHighAccuracy (default
false) — request GPS-level precision. Slower and more power-hungry; on desktop, often makes no difference.timeout (default
Infinity) — milliseconds before the request rejects with code 3.maximumAge (default
0) — accept a cached reading up to this many ms old without re-measuring.
Permission flow
On the first call, the browser shows its own prompt — your code cannot style it or trigger it explicitly. The result is sticky: most browsers remember the choice per origin. You can check the state ahead of time with the Permissions API:
const status = await navigator.permissions.query({ name: "geolocation" });
status.state; // "granted" | "denied" | "prompt"
status.addEventListener("change", () => console.log("now:", status.state));That lets you show a contextual explainer before calling getCurrentPosition — much better than the cold native prompt.
Common UX shape
ask only when the user takes an action
button.addEventListener("click", async () => {
button.disabled = true;
try {
const pos = await getPosition({ enableHighAccuracy: true, timeout: 10_000 });
showWeatherFor(pos.coords);
} catch (err) {
if (err.code === 1) banner("You blocked location access. Enable it in site settings.");
if (err.code === 2) banner("We could not get a location signal.");
if (err.code === 3) banner("Timed out — try again outdoors.");
} finally {
button.disabled = false;
}
});