[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-getcurrentposition-not-a-promise":44,"search-suggestions":60,"quiz-article-getcurrentposition-not-a-promise":109,"path-membership-getcurrentposition-not-a-promise":322},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"01a18647-e9e1-7468-9139-3bd852b4328c","getcurrentposition-not-a-promise","PRACTICE_QUIZ","getCurrentPosition() and its defaults","Seven questions on the Geolocation API's real defaults, its callback shape, and the watchPosition()\u002FclearWatch() pairing.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},7,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,85,89,93,97,101,105],{"slug":62,"name":63,"articles":64},"webdev","Webdev",167,{"slug":66,"name":67,"articles":68},"javascript","Javascript",134,{"slug":70,"name":71,"articles":72},"frontend","Frontend",94,{"slug":74,"name":75,"articles":76},"tutorial","Tutorial",67,{"slug":78,"name":79,"articles":80},"css","Css",50,{"slug":82,"name":83,"articles":84},"performance","Performance",28,{"slug":86,"name":87,"articles":88},"typescript","Typescript",24,{"slug":90,"name":91,"articles":92},"react","React",20,{"slug":94,"name":95,"articles":96},"programming","Programming",17,{"slug":98,"name":99,"articles":100},"browser","Browser",16,{"slug":102,"name":103,"articles":104},"node","Node",14,{"slug":106,"name":107,"articles":108},"html","Html",11,{"id":110,"slug":46,"title":111,"subtitle":52,"excerpt":112,"coverUrl":113,"locale":13,"readingMinutes":114,"publishedAt":115,"viewCount":116,"likeCount":19,"commentCount":19,"author":117,"vertical":122,"topic":123,"tags":125,"_count":130,"playground":132,"body":134,"bodyMd":308,"seo":309,"translationGroupId":312,"series":52,"podcastUrl":52,"verticalId":5,"thread":313,"assessments":315,"translations":318,"quiz":320},"01a18647-e99b-71d4-a0db-6ad3f6778daf","getCurrentPosition() Has No Default Timeout. That's the Trap.","A \"find my location\" button that works instantly on your laptop and hangs forever on a phone with a weak signal isn't flaky — it's getCurrentPosition() doing exactly what its defaults tell it to do. No timeout, no cached position, and a watcher nobody told to stop.","\u002Fmedia\u002Fcovers\u002Fgetcurrentposition-not-a-promise.png",6,"2026-11-04T06:10:25.023Z",29,{"id":118,"name":119,"username":120,"avatarUrl":52,"headline":121},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":66,"name":124},"JavaScript",[126,127,128,129],{"slug":66,"name":67,"color":52},{"slug":62,"name":63,"color":52},{"slug":94,"name":95,"color":52},{"slug":74,"name":75,"color":52},{"assessments":131},1,{"slug":46,"title":133},"getCurrentPosition() — interactive playground",{"blocks":135,"version":131},[136,140,143,146,150,156,159,162,165,169,172,176,179,183,186,189,193,197,200,204,207,211,214,218,221,225,229,232,235,238,241,244,247,250,253,256,259,262,265,268,271,274,277,280,283,290,293,296,299,302],{"id":137,"html":138,"type":139},"b1","\u003Cp>The ticket read: &quot;location button sometimes just spins forever.&quot; Not a crash, not an error toast — just a spinner that never stopped. QA couldn&#39;t reproduce it. It worked every time on the office Wi-Fi, every time on a laptop, every time in the demo to the client.\u003C\u002Fp>","paragraph",{"id":141,"html":142,"type":139},"b2","\u003Cp>It only happened out in the field. On a phone. Somewhere the GPS fix took its time and the Wi-Fi-based positioning fallback was slow to answer. Which meant it was exactly the one environment nobody tested in before shipping.\u003C\u002Fp>",{"id":144,"html":145,"type":139},"b3","\u003Cp>Here&#39;s the part that made it worse: the code had an error handler. Both arguments to \u003Ccode>getCurrentPosition()\u003C\u002Fcode> were filled in, success and failure. By every code-review instinct, this was handled.\u003C\u002Fp>",{"id":147,"html":148,"text":148,"type":149,"level":31},"b4","The code that looks fully handled","heading",{"id":151,"code":152,"type":153,"language":154,"highlight":155},"b5","function useMyLocation(onFound, onFail) {\n  navigator.geolocation.getCurrentPosition(\n    (pos) => onFound(pos.coords),\n    (err) => onFail(err.message)\n  );\n}","code","js",[],{"id":157,"html":158,"type":139},"b6","\u003Cp>A success callback, an error callback, nothing swallowed. If you squint, it even looks defensive — most \u003Ccode>getCurrentPosition()\u003C\u002Fcode> snippets you&#39;ll find in the wild look exactly like this.\u003C\u002Fp>",{"id":160,"html":161,"type":139},"b7","\u003Cp>\u003Cstrong>Before you keep reading: what has to happen for that error callback to fire at all?\u003C\u002Fstrong> Not &quot;what does it do&quot; — when does the browser decide to call it?\u003C\u002Fp>",{"id":163,"html":164,"type":139},"b8","\u003Cp>The honest answer is: only when the browser gives up and reports a \u003Ccode>GeolocationPositionError\u003C\u002Fcode> — permission denied, position unavailable, or a timeout. And a timeout is an \u003Cem>option you pass in\u003C\u002Fem>. If you don&#39;t pass one, there&#39;s nothing to time out. The browser just... keeps trying. Forever, if it has to. Neither callback is under any obligation to ever run.\u003C\u002Fp>",{"id":166,"html":167,"text":168,"type":149,"level":31},"b9","The default timeout is not &quot;some reasonable number.&quot; It&#39;s \u003Ccode>Infinity\u003C\u002Fcode>.","The default timeout is not \"some reasonable number.\" It's Infinity.",{"id":170,"html":171,"type":139},"b10","\u003Cp>\u003Ccode>getCurrentPosition()\u003C\u002Fcode> takes a third, optional \u003Ccode>options\u003C\u002Fcode> argument: \u003Ccode>{ enableHighAccuracy, timeout, maximumAge }\u003C\u002Fcode>. Skip it — like the snippet above does — and you get the spec&#39;s own defaults:\u003C\u002Fp>",{"id":173,"code":174,"type":153,"language":154,"highlight":175},"b11","{ enableHighAccuracy: false, timeout: Infinity, maximumAge: 0 }",[],{"id":177,"html":178,"type":139},"b12","\u003Cp>\u003Ccode>timeout: Infinity\u003C\u002Fcode> means precisely what it says: the call will wait as long as the device needs to produce a position, with no upper bound. On a laptop with recent Wi-Fi positioning data cached nearby, that&#39;s near-instant, so the bug never shows up in testing. On a phone with a weak signal indoors, &quot;as long as it needs&quot; can be minutes — or, if the underlying location provider never calls back at all, forever. There&#39;s no silent fallback, no eventual error. The spinner you built to show \u003Cem>while waiting\u003C\u002Fem> just keeps being accurate.\u003C\u002Fp>",{"id":180,"html":181,"text":182,"type":149,"level":31},"b13","The other default nobody reads: \u003Ccode>maximumAge: 0\u003C\u002Fcode>","The other default nobody reads: maximumAge: 0",{"id":184,"html":185,"type":139},"b14","\u003Cp>\u003Ccode>maximumAge\u003C\u002Fcode> controls whether the browser is allowed to hand back a cached position instead of asking the device for a brand-new fix. The default is \u003Ccode>0\u003C\u002Fcode> milliseconds — meaning no cache is old enough to use, ever. Every single call to \u003Ccode>getCurrentPosition()\u003C\u002Fcode>, even one fired a hundred milliseconds after the last one, forces a fresh GPS\u002FWi-Fi\u002Fcell fix from scratch.\u003C\u002Fp>",{"id":187,"html":188,"type":139},"b15","\u003Cp>That&#39;s rarely what you actually want. A &quot;locate me&quot; button a user double-clicks, or a component that calls \u003Ccode>getCurrentPosition()\u003C\u002Fcode> on mount right after a sibling component just did the same thing, don&#39;t need two independent fixes a tenth of a second apart. Passing a real \u003Ccode>maximumAge\u003C\u002Fcode> — &quot;a position up to 30 seconds old is fine&quot; — turns repeat calls into near-free cache hits instead of two full location requests:\u003C\u002Fp>",{"id":190,"code":191,"type":153,"language":154,"highlight":192},"b16","navigator.geolocation.getCurrentPosition(onFound, onFail, {\n  timeout: 10_000,\n  maximumAge: 30_000,\n});",[],{"id":194,"html":195,"text":196,"type":149,"level":31},"b17","\u003Ccode>watchPosition()\u003C\u002Fcode> has the same shape of bug as a forgotten \u003Ccode>clearInterval\u003C\u002Fcode>","watchPosition() has the same shape of bug as a forgotten clearInterval",{"id":198,"html":199,"type":139},"b18","\u003Cp>\u003Ccode>getCurrentPosition()\u003C\u002Fcode> asks once. \u003Ccode>watchPosition()\u003C\u002Fcode> asks continuously, calling your success callback every time the device&#39;s position changes meaningfully — useful for live tracking, useless the moment nobody&#39;s watching the screen anymore. It returns a watch ID, and that ID is your responsibility:\u003C\u002Fp>",{"id":201,"code":202,"type":153,"language":154,"highlight":203},"b19","const watchId = navigator.geolocation.watchPosition(onMove, onError, {\n  enableHighAccuracy: true,\n});\n\n\u002F\u002F later, when you're done:\nnavigator.geolocation.clearWatch(watchId);",[],{"id":205,"html":206,"type":139},"b20","\u003Cp>Skip that last line — the same way it&#39;s easy to skip \u003Ccode>clearInterval()\u003C\u002Fcode> — and the watch keeps running after the component that started it is gone. In a React component, that&#39;s a \u003Ccode>watchPosition()\u003C\u002Fcode> call in a \u003Ccode>useEffect\u003C\u002Fcode> with no cleanup function: the GPS radio stays active, the callback keeps firing into a component that no longer exists, and the battery drain shows up in a support ticket that has nothing to do with location on its face.\u003C\u002Fp>",{"id":208,"code":209,"type":153,"language":154,"highlight":210},"b21","useEffect(() => {\n  const id = navigator.geolocation.watchPosition(onMove, onError);\n  return () => navigator.geolocation.clearWatch(id);\n}, []);",[],{"id":212,"html":213,"type":139},"b22","\u003Cp>One line in the cleanup function. Easy to write, easy to forget, and nothing in the API warns you that you forgot it.\u003C\u002Fp>",{"id":215,"html":216,"text":217,"type":149,"level":31},"b23","It&#39;s not a Promise, and that&#39;s the last gotcha","It's not a Promise, and that's the last gotcha",{"id":219,"html":220,"type":139},"b24","\u003Cp>\u003Ccode>getCurrentPosition()\u003C\u002Fcode> predates \u003Ccode>fetch()\u003C\u002Fcode>, predates \u003Ccode>Promise\u003C\u002Fcode>, and was never updated to support either — it&#39;s success\u002Ferror callbacks, full stop. You can&#39;t \u003Ccode>await\u003C\u002Fcode> it directly. The fix is a wrapper you write once and reuse everywhere:\u003C\u002Fp>",{"id":222,"code":223,"type":153,"language":154,"highlight":224},"b25","function getPosition(options) {\n  return new Promise((resolve, reject) => {\n    navigator.geolocation.getCurrentPosition(resolve, reject, {\n      enableHighAccuracy: false,\n      timeout: 10_000,\n      maximumAge: 30_000,\n      ...options,\n    });\n  });\n}",[],{"id":226,"code":227,"type":153,"language":154,"highlight":228},"b26","try {\n  const pos = await getPosition();\n  useLocation(pos.coords);\n} catch (err) {\n  if (err.code === err.PERMISSION_DENIED) showPermissionHelp();\n  else if (err.code === err.TIMEOUT) showRetry();\n  else showGenericError();\n}",[],{"id":230,"html":231,"type":139},"b27","\u003Cp>Every default that bit the original snippet is now an explicit choice, and the whole thing reads top to bottom instead of nesting two callbacks inside each other.\u003C\u002Fp>",{"id":233,"html":234,"text":234,"type":149,"level":31},"b28","The one real constraint: it needs a secure origin",{"id":236,"html":237,"type":139},"b29","\u003Cp>This part isn&#39;t a trap, just worth stating plainly: every major browser requires a secure context — HTTPS, or \u003Ccode>localhost\u003C\u002Fcode> in development — before it will even ask for location permission. That&#39;s been true for close to a decade now, not a new restriction, and there&#39;s no workaround or flag to reach for; if a \u003Ccode>getCurrentPosition()\u003C\u002Fcode> call is silently failing in a way none of the above explains, check the protocol in the address bar first.\u003C\u002Fp>",{"id":239,"html":240,"type":139},"b30","\u003C!-- playground:start -->",{"id":242,"html":243,"text":243,"type":149,"level":31},"b31","🎮 Try it yourself",{"id":245,"html":246,"type":139},"b32","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fgetcurrentposition-not-a-promise\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":248,"html":249,"type":139},"b33","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":251,"html":252,"type":139},"b34","\u003C!-- playground:end -->",{"id":254,"html":255,"type":139},"b35","\u003Cp>The playground above simulates the GPS fix with a slider, so you get the exact same race every time instead of waiting on whatever signal your actual device has right now. Set a slow &quot;fix,&quot; turn on &quot;use the default options,&quot; and watch the stopwatch climb with nothing to stop it — that&#39;s \u003Ccode>timeout: Infinity\u003C\u002Fcode>, reproduced on demand. Then try the \u003Ccode>watchPosition()\u003C\u002Fcode> panel: start it, hit &quot;simulate component unmount&quot; without stopping it first, and watch the counter keep climbing anyway.\u003C\u002Fp>",{"id":257,"html":258,"type":139},"b36","\u003C!-- quiz:start -->",{"id":260,"html":261,"text":261,"type":149,"level":31},"b37","🧠 Test yourself",{"id":263,"html":264,"type":139},"b38","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fgetcurrentposition-not-a-promise\u002Fquiz\">Take the 7-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":266,"html":267,"type":139},"b39","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":269,"html":270,"type":139},"b40","\u003C!-- quiz:end -->",{"id":272,"html":273,"type":139},"b41","\u003Cp>The fix for that &quot;spins forever&quot; ticket was three lines: a \u003Ccode>timeout\u003C\u002Fcode>, a \u003Ccode>maximumAge\u003C\u002Fcode>, and a \u003Ccode>clearWatch()\u003C\u002Fcode> in a cleanup function that had never been written. None of it was hard once someone knew to look for it — the hard part was realizing that &quot;sometimes it just hangs&quot; wasn&#39;t a flaky bug report. It was the spec&#39;s own default, read literally.\u003C\u002Fp>",{"id":275,"html":276,"type":139},"b42","\u003Cp>Does your codebase have a bare \u003Ccode>getCurrentPosition()\u003C\u002Fcode> call anywhere without a \u003Ccode>timeout\u003C\u002Fcode>? Worth a five-minute grep before it becomes someone else&#39;s &quot;impossible to reproduce&quot; ticket.\u003C\u002Fp>",{"id":278,"html":279,"type":139},"b43","\u003C!-- related:start -->",{"id":281,"html":282,"text":282,"type":149,"level":31},"b44","📚 Read next",{"id":284,"type":285,"items":286,"ordered":18},"b45","list",[287,288,289],"\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Funhandledrejection-swallowed-promise-errors\">Stop Losing Promise Errors. \u003Ccode>unhandledrejection\u003C\u002Fcode> Catches Them.\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fpromise-try-synchronous-throw-safety\">My .catch() Never Ran. The Bug Was Two Lines Above It\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fweekly-javascript-proxy-reflect\">JavaScript Proxy and Reflect: The Complete Guide\u003C\u002Fa>",{"id":291,"html":292,"type":139},"b46","\u003C!-- related:end -->",{"id":294,"type":295},"b47","divider",{"id":297,"html":298,"type":139},"b48","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":300,"html":301,"type":139},"b49","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":303,"type":285,"items":304,"ordered":18},"b50",[305,306,307],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","The ticket read: \"location button sometimes just spins forever.\" Not a crash, not an error toast — just a spinner that never stopped. QA couldn't reproduce it. It worked every time on the office Wi-Fi, every time on a laptop, every time in the demo to the client.\n\nIt only happened out in the field. On a phone. Somewhere the GPS fix took its time and the Wi-Fi-based positioning fallback was slow to answer. Which meant it was exactly the one environment nobody tested in before shipping.\n\nHere's the part that made it worse: the code had an error handler. Both arguments to `getCurrentPosition()` were filled in, success and failure. By every code-review instinct, this was handled.\n\n## The code that looks fully handled\n\n```js\nfunction useMyLocation(onFound, onFail) {\n  navigator.geolocation.getCurrentPosition(\n    (pos) => onFound(pos.coords),\n    (err) => onFail(err.message)\n  );\n}\n```\n\nA success callback, an error callback, nothing swallowed. If you squint, it even looks defensive — most `getCurrentPosition()` snippets you'll find in the wild look exactly like this.\n\n**Before you keep reading: what has to happen for that error callback to fire at all?** Not \"what does it do\" — when does the browser decide to call it?\n\nThe honest answer is: only when the browser gives up and reports a `GeolocationPositionError` — permission denied, position unavailable, or a timeout. And a timeout is an *option you pass in*. If you don't pass one, there's nothing to time out. The browser just... keeps trying. Forever, if it has to. Neither callback is under any obligation to ever run.\n\n## The default timeout is not \"some reasonable number.\" It's `Infinity`.\n\n`getCurrentPosition()` takes a third, optional `options` argument: `{ enableHighAccuracy, timeout, maximumAge }`. Skip it — like the snippet above does — and you get the spec's own defaults:\n\n```js\n{ enableHighAccuracy: false, timeout: Infinity, maximumAge: 0 }\n```\n\n`timeout: Infinity` means precisely what it says: the call will wait as long as the device needs to produce a position, with no upper bound. On a laptop with recent Wi-Fi positioning data cached nearby, that's near-instant, so the bug never shows up in testing. On a phone with a weak signal indoors, \"as long as it needs\" can be minutes — or, if the underlying location provider never calls back at all, forever. There's no silent fallback, no eventual error. The spinner you built to show *while waiting* just keeps being accurate.\n\n## The other default nobody reads: `maximumAge: 0`\n\n`maximumAge` controls whether the browser is allowed to hand back a cached position instead of asking the device for a brand-new fix. The default is `0` milliseconds — meaning no cache is old enough to use, ever. Every single call to `getCurrentPosition()`, even one fired a hundred milliseconds after the last one, forces a fresh GPS\u002FWi-Fi\u002Fcell fix from scratch.\n\nThat's rarely what you actually want. A \"locate me\" button a user double-clicks, or a component that calls `getCurrentPosition()` on mount right after a sibling component just did the same thing, don't need two independent fixes a tenth of a second apart. Passing a real `maximumAge` — \"a position up to 30 seconds old is fine\" — turns repeat calls into near-free cache hits instead of two full location requests:\n\n```js\nnavigator.geolocation.getCurrentPosition(onFound, onFail, {\n  timeout: 10_000,\n  maximumAge: 30_000,\n});\n```\n\n## `watchPosition()` has the same shape of bug as a forgotten `clearInterval`\n\n`getCurrentPosition()` asks once. `watchPosition()` asks continuously, calling your success callback every time the device's position changes meaningfully — useful for live tracking, useless the moment nobody's watching the screen anymore. It returns a watch ID, and that ID is your responsibility:\n\n```js\nconst watchId = navigator.geolocation.watchPosition(onMove, onError, {\n  enableHighAccuracy: true,\n});\n\n\u002F\u002F later, when you're done:\nnavigator.geolocation.clearWatch(watchId);\n```\n\nSkip that last line — the same way it's easy to skip `clearInterval()` — and the watch keeps running after the component that started it is gone. In a React component, that's a `watchPosition()` call in a `useEffect` with no cleanup function: the GPS radio stays active, the callback keeps firing into a component that no longer exists, and the battery drain shows up in a support ticket that has nothing to do with location on its face.\n\n```js\nuseEffect(() => {\n  const id = navigator.geolocation.watchPosition(onMove, onError);\n  return () => navigator.geolocation.clearWatch(id);\n}, []);\n```\n\nOne line in the cleanup function. Easy to write, easy to forget, and nothing in the API warns you that you forgot it.\n\n## It's not a Promise, and that's the last gotcha\n\n`getCurrentPosition()` predates `fetch()`, predates `Promise`, and was never updated to support either — it's success\u002Ferror callbacks, full stop. You can't `await` it directly. The fix is a wrapper you write once and reuse everywhere:\n\n```js\nfunction getPosition(options) {\n  return new Promise((resolve, reject) => {\n    navigator.geolocation.getCurrentPosition(resolve, reject, {\n      enableHighAccuracy: false,\n      timeout: 10_000,\n      maximumAge: 30_000,\n      ...options,\n    });\n  });\n}\n```\n\n```js\ntry {\n  const pos = await getPosition();\n  useLocation(pos.coords);\n} catch (err) {\n  if (err.code === err.PERMISSION_DENIED) showPermissionHelp();\n  else if (err.code === err.TIMEOUT) showRetry();\n  else showGenericError();\n}\n```\n\nEvery default that bit the original snippet is now an explicit choice, and the whole thing reads top to bottom instead of nesting two callbacks inside each other.\n\n## The one real constraint: it needs a secure origin\n\nThis part isn't a trap, just worth stating plainly: every major browser requires a secure context — HTTPS, or `localhost` in development — before it will even ask for location permission. That's been true for close to a decade now, not a new restriction, and there's no workaround or flag to reach for; if a `getCurrentPosition()` call is silently failing in a way none of the above explains, check the protocol in the address bar first.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fgetcurrentposition-not-a-promise\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\nThe playground above simulates the GPS fix with a slider, so you get the exact same race every time instead of waiting on whatever signal your actual device has right now. Set a slow \"fix,\" turn on \"use the default options,\" and watch the stopwatch climb with nothing to stop it — that's `timeout: Infinity`, reproduced on demand. Then try the `watchPosition()` panel: start it, hit \"simulate component unmount\" without stopping it first, and watch the counter keep climbing anyway.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 7-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fgetcurrentposition-not-a-promise\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\nThe fix for that \"spins forever\" ticket was three lines: a `timeout`, a `maximumAge`, and a `clearWatch()` in a cleanup function that had never been written. None of it was hard once someone knew to look for it — the hard part was realizing that \"sometimes it just hangs\" wasn't a flaky bug report. It was the spec's own default, read literally.\n\nDoes your codebase have a bare `getCurrentPosition()` call anywhere without a `timeout`? Worth a five-minute grep before it becomes someone else's \"impossible to reproduce\" ticket.\n\n\u003C!-- related:start -->\n\n## 📚 Read next\n\n- [Stop Losing Promise Errors. `unhandledrejection` Catches Them.](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Funhandledrejection-swallowed-promise-errors)\n- [My .catch() Never Ran. The Bug Was Two Lines Above It](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fpromise-try-synchronous-throw-safety)\n- [JavaScript Proxy and Reflect: The Complete Guide](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fweekly-javascript-proxy-reflect)\n\n\u003C!-- related:end -->\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":111,"canonical":310,"description":311},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fgetcurrentposition-not-a-promise","A \"find my location\" button that works instantly on your laptop and hangs forever on a phone with a weak signal isn't flaky — it's getCurrentPosition() doing exactly what its defau","01a18647-e99b-71d4-a0db-6e31b5e6f414",{"id":314,"locked":18},"01a18647-e9c5-71ea-87f1-51897599070c",[316],{"id":45,"slug":46,"title":48,"_count":317},{"questions":51},[319],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":321,"questionCount":51},{"questions":51},[]]