JS Geolocation API Guide: A Complete Tutorial

A complete tutorial on the JavaScript Geolocation API. Covers navigator.geolocation, getCurrentPosition, watchPosition, clearWatch, position coordinates, accuracy and altitude, error handling with GeolocationPositionError, permission states with Permissions API, options like enableHighAccuracy and timeout, and building a location service wrapper.

JavaScriptintermediate
14 min read

The Geolocation API lets web applications access the user's geographic location with their permission. It provides one-time lookups, continuous tracking, and detailed position data including coordinates, altitude, speed, and heading. This guide covers every method and error scenario.

Checking for Support

javascriptjavascript
if ("geolocation" in navigator) {
  console.log("Geolocation is supported");
} else {
  console.log("Geolocation is not supported by this browser");
}

Getting Current Position

javascriptjavascript
navigator.geolocation.getCurrentPosition(
  // Success callback
  (position) => {
    console.log("Latitude:", position.coords.latitude);
    console.log("Longitude:", position.coords.longitude);
    console.log("Accuracy:", position.coords.accuracy, "meters");
    console.log("Timestamp:", new Date(position.timestamp));
  },
  // Error callback
  (error) => {
    console.error(`Geolocation error (${error.code}): ${error.message}`);
  },
  // Options
  {
    enableHighAccuracy: true,
    timeout: 10000,
    maximumAge: 0,
  }
);

Position Object Properties

PropertyTypeDescription
coords.latitudenumberLatitude in decimal degrees
coords.longitudenumberLongitude in decimal degrees
coords.accuracynumberAccuracy radius in meters
coords.altitudenumber or nullAltitude in meters above sea level
coords.altitudeAccuracynumber or nullAltitude accuracy in meters
coords.headingnumber or nullDirection of travel in degrees (0-360)
coords.speednumber or nullSpeed in meters per second
timestampnumberTime the position was acquired (ms since epoch)

Options Object

OptionTypeDefaultDescription
enableHighAccuracybooleanfalseRequest GPS-level accuracy (slower, uses more battery)
timeoutnumberInfinityMaximum time (ms) to wait for a position
maximumAgenumber0Accept cached position if younger than this (ms)
javascriptjavascript
// Fast, low-power position (cell tower / Wi-Fi)
const lowAccuracyOptions = {
  enableHighAccuracy: false,
  timeout: 5000,
  maximumAge: 60000, // Accept 1-minute-old cache
};
 
// Precise GPS position (slower, battery-intensive)
const highAccuracyOptions = {
  enableHighAccuracy: true,
  timeout: 15000,
  maximumAge: 0, // Always get fresh position
};
 
// Balance: try high accuracy, fall back to cached
const balancedOptions = {
  enableHighAccuracy: true,
  timeout: 10000,
  maximumAge: 30000, // Accept 30-second cache
};

Error Handling

javascriptjavascript
function handleGeolocationError(error) {
  switch (error.code) {
    case error.PERMISSION_DENIED:
      console.error("User denied geolocation permission");
      showMessage("Please enable location access in your browser settings.");
      break;
    case error.POSITION_UNAVAILABLE:
      console.error("Position information is unavailable");
      showMessage("Unable to determine your location. Check GPS/Wi-Fi.");
      break;
    case error.TIMEOUT:
      console.error("Geolocation request timed out");
      showMessage("Location request timed out. Please try again.");
      break;
    default:
      console.error("Unknown geolocation error:", error.message);
      showMessage("An unexpected error occurred.");
  }
}
 
navigator.geolocation.getCurrentPosition(
  (pos) => console.log(pos.coords.latitude, pos.coords.longitude),
  handleGeolocationError,
  { enableHighAccuracy: true, timeout: 10000 }
);

Error Code Reference

CodeConstantCause
1PERMISSION_DENIEDUser or browser blocked location access
2POSITION_UNAVAILABLEDevice cannot determine position (no GPS/Wi-Fi)
3TIMEOUTPosition request exceeded the timeout option

Watching Position (Continuous Tracking)

javascriptjavascript
let watchId = null;
 
function startTracking() {
  watchId = navigator.geolocation.watchPosition(
    (position) => {
      console.log("Updated position:");
      console.log(`  Lat: ${position.coords.latitude}`);
      console.log(`  Lng: ${position.coords.longitude}`);
      console.log(`  Accuracy: ${position.coords.accuracy}m`);
 
      if (position.coords.speed !== null) {
        console.log(`  Speed: ${position.coords.speed} m/s`);
      }
    },
    (error) => {
      handleGeolocationError(error);
      if (error.code === error.PERMISSION_DENIED) {
        stopTracking();
      }
    },
    {
      enableHighAccuracy: true,
      timeout: 20000,
      maximumAge: 5000,
    }
  );
}
 
function stopTracking() {
  if (watchId !== null) {
    navigator.geolocation.clearWatch(watchId);
    watchId = null;
    console.log("Tracking stopped");
  }
}
 
// Start
startTracking();
 
// Stop after 5 minutes
setTimeout(stopTracking, 5 * 60 * 1000);

For advanced tracking with distance calculation and geofencing, see tracking user location with JavaScript geolocation.

Permission State Detection

javascriptjavascript
async function checkGeolocationPermission() {
  if (!navigator.permissions) {
    return "unknown"; // Permissions API not supported
  }
 
  try {
    const status = await navigator.permissions.query({ name: "geolocation" });
 
    console.log("Current permission:", status.state);
    // "granted", "denied", or "prompt"
 
    // Listen for permission changes
    status.addEventListener("change", () => {
      console.log("Permission changed to:", status.state);
    });
 
    return status.state;
  } catch {
    return "unknown";
  }
}
 
// Conditional UI based on permission state
async function showLocationUI() {
  const permission = await checkGeolocationPermission();
 
  switch (permission) {
    case "granted":
      // Silently get location without prompt
      getLocation();
      break;
    case "denied":
      showSettingsInstructions();
      break;
    case "prompt":
      showLocationRequestButton();
      break;
    default:
      showLocationRequestButton();
  }
}

Promise-Based Wrapper

javascriptjavascript
function getPosition(options = {}) {
  return new Promise((resolve, reject) => {
    if (!("geolocation" in navigator)) {
      reject(new Error("Geolocation is not supported"));
      return;
    }
 
    navigator.geolocation.getCurrentPosition(resolve, reject, options);
  });
}
 
// Usage with async/await
async function showUserLocation() {
  try {
    const position = await getPosition({
      enableHighAccuracy: true,
      timeout: 10000,
    });
 
    const { latitude, longitude, accuracy } = position.coords;
    console.log(`Location: ${latitude}, ${longitude} (+/- ${accuracy}m)`);
  } catch (error) {
    handleGeolocationError(error);
  }
}

Location Service Wrapper

javascriptjavascript
class LocationService {
  constructor(defaultOptions = {}) {
    this.options = {
      enableHighAccuracy: true,
      timeout: 10000,
      maximumAge: 0,
      ...defaultOptions,
    };
    this.watchId = null;
    this.lastPosition = null;
    this.listeners = new Set();
  }
 
  async getPermission() {
    if (!navigator.permissions) return "unknown";
    try {
      const result = await navigator.permissions.query({ name: "geolocation" });
      return result.state;
    } catch {
      return "unknown";
    }
  }
 
  getCurrentPosition(options = {}) {
    return new Promise((resolve, reject) => {
      navigator.geolocation.getCurrentPosition(
        (position) => {
          this.lastPosition = position;
          resolve(position);
        },
        reject,
        { ...this.options, ...options }
      );
    });
  }
 
  startWatching(options = {}) {
    if (this.watchId !== null) {
      this.stopWatching();
    }
 
    this.watchId = navigator.geolocation.watchPosition(
      (position) => {
        this.lastPosition = position;
        this.notifyListeners(position);
      },
      (error) => {
        this.notifyListeners(null, error);
      },
      { ...this.options, ...options }
    );
  }
 
  stopWatching() {
    if (this.watchId !== null) {
      navigator.geolocation.clearWatch(this.watchId);
      this.watchId = null;
    }
  }
 
  onPositionUpdate(callback) {
    this.listeners.add(callback);
    return () => this.listeners.delete(callback);
  }
 
  notifyListeners(position, error = null) {
    for (const listener of this.listeners) {
      listener(position, error);
    }
  }
 
  getLastKnown() {
    return this.lastPosition;
  }
 
  isWatching() {
    return this.watchId !== null;
  }
 
  async getCoordinates(options = {}) {
    const position = await this.getCurrentPosition(options);
    return {
      latitude: position.coords.latitude,
      longitude: position.coords.longitude,
      accuracy: position.coords.accuracy,
    };
  }
 
  destroy() {
    this.stopWatching();
    this.listeners.clear();
    this.lastPosition = null;
  }
}
 
// Usage
const location = new LocationService({ enableHighAccuracy: true });
 
const coords = await location.getCoordinates();
console.log(coords); // { latitude: 30.26, longitude: -97.74, accuracy: 15 }
 
// Continuous tracking
const unsub = location.onPositionUpdate((pos, err) => {
  if (err) {
    console.error("Location error:", err.message);
    return;
  }
  console.log(`Moved to: ${pos.coords.latitude}, ${pos.coords.longitude}`);
});
 
location.startWatching();
 
// Later
unsub();
location.destroy();
Rune AI

Rune AI

Key Insights

  • Always handle errors: Users can deny permission, GPS can be unavailable, and requests can time out; handle all three error codes
  • Permission state check first: Use the Permissions API to check the current state before calling getCurrentPosition to avoid unnecessary prompts
  • enableHighAccuracy trades speed for precision: GPS is 3-10m accurate but slower and power-hungry; Wi-Fi/cell is faster but 20-3000m accuracy
  • watchPosition for continuous tracking: Returns a watchId for clearWatch; always stop watching when no longer needed to save battery
  • HTTPS required: Geolocation is blocked on non-secure origins; localhost is the only exception for development
RunePowered by Rune AI

Frequently Asked Questions

Does the Geolocation API work over HTTP?

No. Modern browsers require HTTPS (or localhost) for geolocation access. On HTTP pages, `navigator.geolocation` exists but `getCurrentPosition` immediately fails with `PERMISSION_DENIED`. Always serve your site over HTTPS.

How accurate is browser geolocation?

It depends on the source. GPS provides 3-10 meter accuracy. Wi-Fi positioning gives 20-50 meters. Cell tower triangulation gives 100-3000 meters. Set `enableHighAccuracy: true` to request GPS when available. Check `coords.accuracy` to know the actual accuracy radius.

Can I get a user's location without their permission?

No. The browser always prompts the user before sharing location data. There is no way to bypass this prompt. If the user denies permission, the error callback fires with `PERMISSION_DENIED`. Respect user privacy and provide fallbacks.

What is the difference between getCurrentPosition and watchPosition?

`getCurrentPosition` returns a single position and stops. `watchPosition` continuously monitors the position and calls the success callback every time the location changes or improves in accuracy. Use `clearWatch(watchId)` to stop watching.

How do I handle the case where the user has denied permission permanently?

Check the permission state using the Permissions API (`navigator.permissions.query`). If the state is `"denied"`, show instructions for enabling location in browser settings. There is no way to re-prompt programmatically after permanent denial.

Conclusion

The Geolocation API provides one-time and continuous location access with detailed coordinate data. Always handle all three error codes, check permission state before prompting, use enableHighAccuracy only when needed to save battery, and wrap callbacks in Promises for cleaner async/await code. For continuous tracking with distance and geofencing, see tracking user location with JavaScript geolocation. For browser storage of location data, see JS localStorage API guide.