../

Geolocation

navigator.geolocation for one-off and continuous positions: options, the coordinate and error types, permissions, privacy, and distance math. Support data is from MDN as of September 2026.

Support & requirements

FeatureStatus (MDN)Notes
getCurrentPosition, watchPosition, clearWatchBaseline widely available (2015)every browser
Permissions API "geolocation"all current enginesSafari 16+; change never fires in Safari
GeolocationPosition.toJSON()Chrome 126, Firefox 129, Safari 18lets JSON.stringify(pos) work
<geolocation> elementChromium 144+a browser-owned "use my location" button
RequirementDetail
secure contextHTTPS or localhost; on HTTP the call fails with PERMISSION_DENIED
Permissions Policygeolocation; cross-origin iframes need allow="geolocation"
visible pageno background tracking; updates stop when the tab or app is hidden
user promptshown on the first call; no separate "request" method

API

MemberReturnsNotes
getCurrentPosition(success, error?, options?)voidone fix; callbacks, not a promise
watchPosition(success, error?, options?)numberwatch ID; success fires on every change
clearWatch(id)voidstops a watch; always call it on cleanup
navigator.geolocation.getCurrentPosition(
  (pos) => {
    const { latitude, longitude, accuracy } = pos.coords;
    console.log(latitude, longitude, `±${accuracy} m`);
  },
  (err) => console.error(err.code, err.message),
  { enableHighAccuracy: false, timeout: 10_000 },
);

Options

OptionTypeDefaultMeaning
enableHighAccuracybooleanfalseask for GPS-grade fixes; slower and uses more battery
timeoutmsInfinitymax wait for a fix; the clock starts after the permission prompt
maximumAgems0accept a cached fix this old; Infinity = any cached fix
GoalOptions
"near me" search, city-level{ maximumAge: 600_000, timeout: 5_000 }
show a pin on a map{ enableHighAccuracy: false, timeout: 10_000 }
turn-by-turn, run trackingwatchPosition with { enableHighAccuracy: true, maximumAge: 0 }
instant answer if one is cached{ maximumAge: Infinity, timeout: 0 }: TIMEOUT if none

Position & coordinates

GeolocationPosition has coords: GeolocationCoordinates and timestamp (epoch ms).

FieldTypeUnit / meaning
latitudenumberdegrees, WGS 84, −90 to 90
longitudenumberdegrees, WGS 84, −180 to 180
accuracynumbermeters; radius with 95% confidence
altitudenumber | nullmeters above the WGS 84 ellipsoid (not sea level)
altitudeAccuracynumber | nullmeters, 95% confidence
headingnumber | nulldegrees clockwise from true north; NaN when speed is 0
speednumber | nullmeters per second

null means the device can't measure it, typical for altitude, heading and speed on laptops. The fields are getters on the prototype, so { ...pos.coords } is {}. Copy the fields you need or call pos.toJSON() before storing or posting a position.

Decimal placesPrecision at the equatorGood for
1~11 kmregion
2~1.1 kmtown, analytics
3~110 mneighborhood
4~11 mstreet, building
5~1.1 mtree, door
6~0.11 mmore than any phone gives

Errors

The error callback gets a GeolocationPositionError.

codeConstantCause
1PERMISSION_DENIEDuser or OS said no, insecure context, or blocked by Permissions Policy
2POSITION_UNAVAILABLEno fix: no GPS signal, location services off, provider error
3TIMEOUTno fix within timeout

message is for logs only; its text varies by browser. Without an error callback, failures are silent. On macOS and Windows, a denied OS-level location permission for the browser also surfaces as code 1 or 2, so tell users to check system settings too.

Permissions

const status = await navigator.permissions.query({
  name: "geolocation",
});
// "granted" | "denied" | "prompt"
status.addEventListener("change", () => {
  console.log(status.state); // never fires in Safari
});
StateUI to show
prompta "Use my location" button; call the API from its click handler
grantedlocate straight away if the feature needs it
deniedmanual fallback (postcode or city search) and a "how to re-enable" hint

Browsers remember the choice per origin. Safari may re-prompt per session or per day depending on the user's setting; Chrome embargoes an origin after repeated dismissals. iOS and Android let users grant approximate location only; the API still works, with accuracy in kilometers.

Accuracy & battery

SourceTypical accuracyNotes
GNSS (GPS etc.)3–10 m outdoorsneeds enableHighAccuracy; slow first fix, drains battery
Wi-Fi15–50 mthe usual source indoors and on laptops
cell towers100 m–few kmphones without Wi-Fi
IP addresscity or worsedesktop fallback; VPNs make it wrong
  • Check accuracy before trusting a fix: drop points with a large radius when drawing a track.
  • watchPosition may fire often with tiny changes; throttle, or ignore moves smaller than accuracy.
  • Stop watching when the view closes or the page is hidden; see Page Visibility.
  • The first high-accuracy fix can take tens of seconds; show a coarse fix first, then refine.

Privacy

PracticeWhy
ask only from a user actionprompts on load get denied, and denial sticks
say why before the prompt"Find stores near you" beats a bare browser dialog
round before sending or storing2–3 decimals is enough for "nearby" and is far less identifying
keep it client-side when possiblesort or filter on the device, send only the result
don't log raw coordinatesprecise location is personal data (GDPR, CCPA)
offer a manual alternativesearch by city or postcode for users who say no
use server-side IP geolocation for countryno prompt needed for locale or currency defaults

Typing in TS

TypeNotes
GeolocationPositioncoords, timestamp, toJSON()
GeolocationCoordinatesfields above; nullable ones typed number | null
GeolocationPositionErrorcode: number plus static constants
PositionOptionsthe options object
PositionCallback, PositionErrorCallbackcallback types
function describe(err: GeolocationPositionError): string {
  switch (err.code) {
    case GeolocationPositionError.PERMISSION_DENIED:
      return "Location access is off";
    case GeolocationPositionError.POSITION_UNAVAILABLE:
      return "Couldn't find your location";
    case GeolocationPositionError.TIMEOUT:
      return "Finding your location took too long";
    default:
      return err.message;
  }
}

navigator.geolocation is typed as always present; it is, but in an insecure context every call fails, so check window.isSecureContext for a better message.

Distance & bearing

The haversine formula gives great-circle distance on a sphere of radius RR (mean Earth radius 6,371 km). It is within about 0.5% of the ellipsoidal answer, fine for "3.2 km away".

a=sin⁡2 ⁣Δφ2+cos⁡φ1cos⁡φ2sin⁡2 ⁣Δλ2,d=2Ratan2⁡ ⁣(a,1−a)a = \sin^2\!\frac{\Delta\varphi}{2} + \cos\varphi_1 \cos\varphi_2 \sin^2\!\frac{\Delta\lambda}{2}, \qquad d = 2R \operatorname{atan2}\!\left(\sqrt{a}, \sqrt{1-a}\right)

φ\varphi is latitude and λ\lambda longitude, both in radians. Initial bearing from point 1 to 2:

θ=atan2⁡ ⁣(sin⁡Δλcos⁡φ2,  cos⁡φ1sin⁡φ2−sin⁡φ1cos⁡φ2cos⁡Δλ)\theta = \operatorname{atan2}\!\left(\sin\Delta\lambda \cos\varphi_2,\; \cos\varphi_1 \sin\varphi_2 - \sin\varphi_1 \cos\varphi_2 \cos\Delta\lambda\right)
Rule of thumbValue
1° of latitude~111.2 km everywhere
1° of longitude~111.3 km × cos(latitude)
0.001° of latitude~111 m

For routes, areas or survey-grade distances, use a library (Turf.js, geographiclib) or PostGIS; see Postgres.

Recipes

Promisified getCurrentPosition

await a position, with cancellation through an AbortSignal.

get-position.ts
export function getPosition(
  options: PositionOptions = {},
  signal?: AbortSignal,
): Promise<GeolocationPosition> {
  return new Promise((resolve, reject) => {
    if (!window.isSecureContext) {
      reject(new Error("Geolocation needs HTTPS"));
      return;
    }
    signal?.throwIfAborted();
    signal?.addEventListener("abort", () =>
      reject(signal.reason),
    );
    navigator.geolocation.getCurrentPosition(
      resolve,
      reject, // GeolocationPositionError
      options,
    );
  });
}

The underlying request can't be canceled; abort only stops you waiting for it.

Watch with cleanup

Track movement and return a disposer, so callers can't leak the watch.

type Fix = { lat: number; lng: number; accuracy: number };
 
export function watchLocation(
  onFix: (fix: Fix) => void,
  onError: (err: GeolocationPositionError) => void,
  maxAccuracy = 100, // meters
): () => void {
  const id = navigator.geolocation.watchPosition(
    ({ coords }) => {
      if (coords.accuracy > maxAccuracy) return;
      onFix({
        lat: coords.latitude,
        lng: coords.longitude,
        accuracy: coords.accuracy,
      });
    },
    onError,
    { enableHighAccuracy: true, maximumAge: 5_000 },
  );
  return () => navigator.geolocation.clearWatch(id);
}

Watch from a React hook

The same watch tied to a component's lifetime.

import { useEffect, useState } from "react";
 
export function useGeolocation(options?: PositionOptions) {
  const [pos, setPos] = useState<GeolocationPosition>();
  const [error, setError] =
    useState<GeolocationPositionError>();
 
  useEffect(() => {
    const id = navigator.geolocation.watchPosition(
      setPos,
      setError,
      options,
    );
    return () => navigator.geolocation.clearWatch(id);
    // pass a stable (memoised) options object
  }, [options]);
 
  return { pos, error };
}

Haversine distance

Distance in meters between two points, for "2.4 km away" labels or sorting by nearest.

haversine.ts
export type LatLng = { lat: number; lng: number };
 
const R = 6_371_008.8; // mean Earth radius, meters
const rad = (deg: number) => (deg * Math.PI) / 180;
 
export function distanceM(a: LatLng, b: LatLng): number {
  const dLat = rad(b.lat - a.lat);
  const dLng = rad(b.lng - a.lng);
  const h =
    Math.sin(dLat / 2) ** 2 +
    Math.cos(rad(a.lat)) *
      Math.cos(rad(b.lat)) *
      Math.sin(dLng / 2) ** 2;
  return 2 * R * Math.asin(Math.min(1, Math.sqrt(h)));
}
 
// London to Paris: ~343.5 km
distanceM(
  { lat: 51.5074, lng: -0.1278 },
  { lat: 48.8566, lng: 2.3522 },
);

Sort places by distance

Nearest-first list after a single fix; the math runs on the device.

import { distanceM, type LatLng } from "./haversine.ts";
import { getPosition } from "./get-position.ts";
 
type Place = LatLng & { name: string };
 
export async function nearest(places: Place[], n = 5) {
  const { coords } = await getPosition({
    maximumAge: 600_000,
    timeout: 8_000,
  });
  const me = { lat: coords.latitude, lng: coords.longitude };
  return places
    .map((p) => ({ ...p, meters: distanceM(me, p) }))
    .sort((a, b) => a.meters - b.meters)
    .slice(0, n);
}

References