The definitive guide to the JavaScript Geolocation API
By Flavio Copes
Learn the JavaScript Geolocation API with getCurrentPosition(), watchPosition(), permissions, accuracy, timeouts, errors, privacy, and testing.
The Geolocation API lets a web page ask the browser for the user’s location.
You might use it to center a map, find nearby stores, attach a location to a delivery, or follow a trip while the page remains open.
The browser does not give a site direct access to GPS hardware. It returns the best position available from the device and operating system. That position may come from GPS, Wi-Fi, mobile networks, or another source.
Location is sensitive data. Ask only when it clearly improves a task, explain why you need it, and keep it for no longer than necessary.
Check whether geolocation is available
The API lives at navigator.geolocation:
if (!('geolocation' in navigator)) {
throw new Error('Geolocation is not available')
}
Geolocation is a powerful feature and is restricted to secure contexts. Serve production pages over HTTPS. Browsers generally allow localhost during development.
Permissions Policy can also block the feature, especially inside an iframe.
Get the current position
Call getCurrentPosition() with a success callback:
navigator.geolocation.getCurrentPosition((position) => {
console.log(position.coords.latitude)
console.log(position.coords.longitude)
})
The first request may display a browser permission prompt. The browser calls your function only after it obtains a position.
Make this request after a clear user action:
<button id="find-me" type="button">Use my location</button>
<p id="result"></p>
const button = document.querySelector('#find-me')
const result = document.querySelector('#result')
button.addEventListener('click', () => {
result.textContent = 'Finding your location...'
navigator.geolocation.getCurrentPosition((position) => {
const { latitude, longitude } = position.coords
result.textContent = `${latitude}, ${longitude}`
})
})
Do not request location on page load. The user should know which feature needs it and what will happen next.
Understand the position object
The callback receives a GeolocationPosition with two main properties:
coords, the measured coordinates and related valuestimestamp, the time when the position was acquired
The coordinates object includes:
latitudeandlongitudein decimal degreesaccuracyin metersaltitudein meters above the reference ellipsoid, ornullaltitudeAccuracyin meters, ornullheadingin degrees clockwise from true north, ornullspeedin meters per second, ornull
Do not assume altitude, heading, or speed will exist. A laptop connected through Wi-Fi will often provide only latitude, longitude, and accuracy.
Accuracy is not a guarantee that the user is exactly inside a circle. It is an uncertainty estimate. Show that uncertainty when precision matters.
const { latitude, longitude, accuracy } = position.coords
console.log(`Position: ${latitude}, ${longitude}`)
console.log(`Accuracy: about ${Math.round(accuracy)} meters`)
The timestamp uses milliseconds since the Unix epoch. You can turn it into a Date:
const measuredAt = new Date(position.timestamp)
My JavaScript Dates guide explains timestamps and date formatting in detail.
Always add an error callback
A location request can fail. Pass a second callback:
navigator.geolocation.getCurrentPosition(
showPosition,
showError,
)
The error has one of three numeric codes:
PERMISSION_DENIEDPOSITION_UNAVAILABLETIMEOUT
Handle them explicitly:
function showError(error) {
if (error.code === error.PERMISSION_DENIED) {
result.textContent = 'Location access was not allowed'
} else if (error.code === error.POSITION_UNAVAILABLE) {
result.textContent = 'Your location is currently unavailable'
} else if (error.code === error.TIMEOUT) {
result.textContent = 'Finding your location took too long'
}
}
Permission denial can come from the user, browser settings, operating-system settings, an insecure page, or policy. Avoid assuming the user deliberately clicked Block.
Always provide a manual alternative. Let someone type a city, postcode, or address instead of forcing location access.
Configure accuracy, cache, and timeout
The third argument is a PositionOptions object:
const options = {
enableHighAccuracy: false,
maximumAge: 60_000,
timeout: 10_000,
}
navigator.geolocation.getCurrentPosition(
showPosition,
showError,
options,
)
The options solve different problems.
enableHighAccuracy
enableHighAccuracy: true asks the device to prefer a more accurate result when possible.
It can take longer and consume more power. It does not force GPS and does not guarantee a particular accuracy.
Use it for turn-by-turn movement or placing a point precisely. Leave it false when approximate location is enough.
maximumAge
maximumAge says how old a cached position may be, in milliseconds.
{ maximumAge: 5 * 60_000 }
This accepts a position acquired within the last five minutes. A nearby-store search may not need a fresh GPS measurement every time.
Use 0 when you do not want a cached position. Use Infinity when any cached position is acceptable.
timeout
timeout limits how long the browser may take, in milliseconds.
{ timeout: 8_000 }
Without a practical timeout, an interface can appear stuck while the device tries to find a position. A timeout should lead to a useful fallback, not a dead end.
Return a fast result, then improve it
Some interfaces benefit from a two-step strategy.
First accept a recent cached position with a short timeout:
navigator.geolocation.getCurrentPosition(
showApproximatePosition,
showError,
{
maximumAge: 10 * 60_000,
timeout: 2000,
},
)
Then request a fresh, more accurate position in the background:
navigator.geolocation.getCurrentPosition(
showPrecisePosition,
() => {},
{
enableHighAccuracy: true,
maximumAge: 0,
timeout: 15_000,
},
)
This can make a map useful quickly without pretending the first result is final.
Only use the pattern when the interface can explain changing results. A delivery confirmation should not silently replace a selected address because a later measurement moved the marker.
Canceling a one-shot getCurrentPosition() request is not part of the API. If the user leaves the screen, ignore a late callback with application state:
let active = true
navigator.geolocation.getCurrentPosition((position) => {
if (!active) return
showPosition(position)
})
function closeLocationStep() {
active = false
}
Use watchPosition() plus clearWatch() when cancelable repeated updates fit the task better.
Create a promise wrapper
The Geolocation API uses callbacks. A small wrapper makes it easier to use with async and await:
function getCurrentPosition(options = {}) {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
resolve,
reject,
options,
)
})
}
Now you can write:
try {
const position = await getCurrentPosition({
maximumAge: 60_000,
timeout: 10_000,
})
console.log(position.coords.latitude)
} catch (error) {
showError(error)
}
Keep the wrapper local. It does not change permission or accuracy behavior.
Watch a changing position
Use watchPosition() when the page needs repeated updates:
const watchId = navigator.geolocation.watchPosition(
(position) => {
updateMarker(position.coords)
},
showError,
{
enableHighAccuracy: true,
maximumAge: 5_000,
timeout: 15_000,
},
)
It returns an ID. Save it so you can stop the watch:
navigator.geolocation.clearWatch(watchId)
Always clear a watch when the task ends, the user presses Stop, or the component is removed.
let watchId
function startTracking() {
watchId = navigator.geolocation.watchPosition(updatePosition, showError)
}
function stopTracking() {
if (watchId !== undefined) {
navigator.geolocation.clearWatch(watchId)
watchId = undefined
}
}
Continuous high-accuracy tracking can use significant battery. Do not leave it running behind an unrelated screen.
Ignore positions that are not useful
A watch can produce measurements with very different accuracy. Filter them according to the job:
function updatePosition(position) {
if (position.coords.accuracy > 100) {
result.textContent = 'Waiting for a more accurate position...'
return
}
updateMarker(position.coords)
}
Do not choose an arbitrary threshold for every application. A weather page may work with kilometers of uncertainty. A pickup point may need tens of meters.
Also consider age:
const age = Date.now() - position.timestamp
if (age > 30_000) {
return
}
If you draw a moving path, noisy measurements can make the marker jump. Smoothing is an application concern. Keep the raw accuracy visible while tuning it.
Check permission state carefully
Where supported, the Permissions API can report the current geolocation permission state:
const permission = await navigator.permissions.query({
name: 'geolocation',
})
console.log(permission.state)
The state is granted, denied, or prompt.
This can help explain an existing denial or update the interface when settings change:
permission.addEventListener('change', () => {
console.log(permission.state)
})
Do not use this as a substitute for the actual request. Browser and operating-system behavior still varies, and a granted state does not promise that a position can be acquired.
Geolocation inside an iframe
The top-level page can control geolocation with Permissions Policy.
An iframe that should be allowed to request it needs an allow attribute:
<iframe
src="https://maps.example.org/picker"
allow="geolocation"
></iframe>
The server can restrict the feature further with a Permissions-Policy response header.
Only delegate location to origins you trust. An iframe still needs to request user permission.
Protect location data
Coordinates can reveal a home, workplace, medical visit, or daily routine.
Before storing or sending a position, decide:
- whether exact coordinates are truly required
- how long the data is needed
- who can access it
- whether approximate or rounded coordinates are enough
- how the user can remove it
Do not put coordinates in analytics events, public URLs, or logs by accident.
If a nearby search can happen in the browser, keep the raw position there. If the server needs it, send it only to the endpoint performing that task.
Permission is not consent for unrelated reuse. Tell the user what you do with the result.
Test without physically moving
Browser developer tools can override the reported location. Use this to test:
- a normal nearby position
- a position in another country
- low accuracy
- permission denial
- an unavailable position
- a timeout
Also test the manual fallback. It should not be a second-class path.
Keep location-dependent business logic separate from the browser call. A function that accepts { latitude, longitude } is easy to test with fixed values.
function distanceFromShop(coords) {
// calculate using supplied coordinates
}
Then the Geolocation API is only one source of those coordinates.
Calculate distance between two coordinates
Latitude and longitude are angles on the Earth, not flat Cartesian coordinates. Subtracting them does not produce a reliable distance in meters.
For nearby searches, use the haversine formula:
function distanceInMeters(from, to) {
const earthRadius = 6_371_000
const toRadians = (degrees) => degrees * Math.PI / 180
const latitude1 = toRadians(from.latitude)
const latitude2 = toRadians(to.latitude)
const latitudeDelta = toRadians(to.latitude - from.latitude)
const longitudeDelta = toRadians(to.longitude - from.longitude)
const a =
Math.sin(latitudeDelta / 2) ** 2 +
Math.cos(latitude1) *
Math.cos(latitude2) *
Math.sin(longitudeDelta / 2) ** 2
const angle = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
return earthRadius * angle
}
Now compare the measured location with a shop:
const distance = distanceInMeters(
position.coords,
{ latitude: 45.4642, longitude: 9.19 },
)
This gives straight-line distance over the globe. It does not follow streets, trails, or transit routes. Use a routing service when the user needs travel distance or duration.
Do not display more precision than the source supports. If accuracy is 500 meters, showing a distance of 143.27 m creates false confidence.
Round coordinates when exact location is unnecessary
Decimal places imply precision. Roughly speaking, four decimal places of latitude represent about 11 meters, while two represent about one kilometer. Longitude varies with latitude.
For regional personalization or coarse grouping, round before sending or storing:
const approximate = {
latitude: Number(position.coords.latitude.toFixed(2)),
longitude: Number(position.coords.longitude.toFixed(2)),
}
Rounding is not complete anonymization. A location can still identify someone when combined with time or other data. It only reduces precision.
When I would use it
I would use geolocation after someone presses “Use my location” on a store finder, map, or delivery form. I would cache a position briefly when that makes the interaction faster, and I would show a manual address field beside it.
I would use watchPosition() only while a visible feature needs movement. When the user leaves that feature, I would clear the watch.
I would not request exact location to guess a country, personalize generic content, or collect analytics. Locale, timezone, or an explicit choice is often enough and asks for much less trust.
The current W3C Geolocation specification defines position acquisition, errors, options, permissions, and privacy requirements.
Related posts about platform: