Request options and errors
Every service call takes the same options on top of its own parameters: request validation, overrides of the global configuration and a signal to cancel it. Each call runs these steps in order:
- An already-aborted
signalrejects at once, before any work. - The global configuration is merged under the call’s parameters.
- The resulting
commonBaseURLis checked, even withvalidateRequest: false. An insecure one rejects withINSECURE_BASE_URL. - The parameters are validated, unless
validateRequest: false. - The request is built.
- The request is sent, and a
429is retried as the call’s and the globalretryset. - The response is parsed.
Request validation
Before sending, the SDK checks the parameters against the service’s schema. A malformed call fails early with a message in the SDK’s terms, and never reaches the API or your usage quota.
import { discoverPlaces } from '@tomtom-org/maps-sdk/services';
await discoverPlaces({ query: 'Eiffel Tower', validateRequest: false });validateRequest: falseskips the check for that call. This saves CPU when you know the inputs are good.- A failed check rejects with an
SDKErrorwhoseissueslists the Zod issues. - Keep it on while developing.
Per-call overrides
You can pass the API key, language, geopolitical view, base URL and retry behaviour of the global configuration to a single call instead. They override the global values for that call only:
// French for this call only; later calls keep the global languageawait discoverPlaces({ query: 'restaurants', language: 'fr-FR' });
// Give up on a rate-limited search after 2 seconds; the global wait and factor still applyawait getSearchSuggestions({ query: 'coffee', geoBias: { position: [4.9041, 52.3676] }, retry: { timeoutMs: 2000 } });- A call’s
retryoverrides the globalretryfield by field, soretry: { timeoutMs: 2000 }keeps the global wait and factor. To turn retrying off for one call, passretry: undefined. - Each service fixes its own endpoint path and API version. You can’t override either.
- To send every request through a proxy that holds the key server-side, set the global
commonBaseURL. See Proxying your API calls.
Cancelling a request
Every call accepts a signal, an AbortSignal. Aborting it cancels the HTTP request and rejects the call with an SDKAbortError, whose name is 'AbortError'.
- Aborting also cancels a pending
429retry, so an aborted call never waits out its backoff. - An already-aborted signal rejects before the request is built, so nothing is sent.
discoverOnePlaceandgeocodeOnetake the signal in their params form, as indiscoverOnePlace({ query, signal }).calculateReachableRangestakes it as a second argument, for all its ranges at once.
Keeping only the latest request
When a new request supersedes an older one (a click lookup, a search-as-you-type box, a search re-run on every map move), a slow earlier response can land after a faster later one and overwrite it.
createLatestRequest holds one such request at a time. Each run aborts the one before it and hands your callback a fresh signal. cancel aborts without starting another:
import { createLatestRequest, reverseGeocode } from '@tomtom-org/maps-sdk/services';
const latestLookup = createLatestRequest();
map.mapLibreMap.on('click', async (event) => { const lookup = await latestLookup.run((signal) => reverseGeocode({ position: event.lngLat.toArray(), signal }) ); if (!lookup.current) return; // a newer click or a cancel retired it showAddress(lookup.value);});
clearButton.addEventListener('click', latestLookup.cancel);- A retired request resolves
{ current: false }and never rejects, so there is noSDKAbortErrorto catch. - An answer that lands after a
cancelis also dropped. A baresignalcan’t do this once the response is in. - It rethrows the current request’s errors, including a deadline composed onto the signal, so a timeout still reads as a failure.
- One callback can chain several calls on the same signal. A newer run drops the whole chain, wherever it got to.
isRunning()reports whether a request is in flight. Use onecreateLatestRequest()per thing that can be superseded independently.- It cancels, but it does not debounce. Every
runstill sends its request. To send fewer while the user types or drags, debounce the input and callrunfrom the debounced handler.
Deadlines
There is no timeout option. Instead, compose a deadline onto the signal, alone or with your own cancellation:
import { calculateRoute, createLatestRequest, getSearchSuggestions } from '@tomtom-org/maps-sdk/services';
// A short deadline for a search boxawait getSearchSuggestions({ query: 'coffee', geoBias: { position: [4.9041, 52.3676] }, signal: AbortSignal.timeout(3000)});
// A longer one for a route, still retired by the next runconst latestRoute = createLatestRequest();const route = await latestRoute.run((signal) => calculateRoute({ locations: [[4.9041, 52.3676], [2.3522, 48.8566]], signal: AbortSignal.any([signal, AbortSignal.timeout(15000)]) }));- A deadline that fires rejects with
SDKAbortError, like a manual abort. Itscausetells the two apart. It holds the signal’s reason, which is aTimeoutErrorforAbortSignal.timeout. - Under
createLatestRequest, the deadline is the only abort that rejects. A retired run resolves{ current: false }. - The global
retry.timeoutMsis unrelated. It limits how long the SDK retries a429.
Error handling
A service call rejects with an SDKError. A request the service can’t build at all, such as a geometry type it does not search sent with validateRequest: false, rejects with a plain Error instead. An SDKError’s code says what went wrong, service names the service that failed as a ServiceName such as 'Geocode' or 'ReachableRange', and sdkVersion gives the SDK version for bug reports:
code | When |
|---|---|
INVALID_REQUEST | The SDK’s request validation failed (issues lists why), or the API answered 400 or another 4xx not listed below |
INSECURE_BASE_URL | commonBaseURL is neither https: nor http: on a loopback address |
INVALID_CREDENTIALS | The API answered 401, because the key is missing, invalid or expired |
INSUFFICIENT_PERMISSIONS | The API answered 403, because the key is valid but not allowed this request. See A key refused access |
NOT_FOUND | The API answered 404 |
RATE_LIMITED | The API still answered 429 after the retries |
LIVE_SERVICE_NOT_AVAILABLE | The API answered a 5xx |
NETWORK_NOT_AVAILABLE | No response arrived: offline, DNS, TLS or CORS failure. cause holds what fetch threw |
ABORTED | The signal was aborted. cause holds the abort reason |
INTERNAL | Anything else, such as a response the SDK could not read |
Two subclasses carry more:
| Error | When | Carries |
|---|---|---|
SDKServiceError | The API answered with an error status | status, apiErrorCode (the API’s own name for the error, when it gave one) and privatePreviewAccess |
SDKAbortError | The signal was aborted | cause, the abort reason |
discoverOnePlace and geocodeOne also throw a plain Error when nothing matches the query. To get an empty result instead, use discoverPlaces or geocode.
import { discoverPlaces, SDKError } from '@tomtom-org/maps-sdk/services';
try { const results = await discoverPlaces({ query: 'coffee', geoBias: { position: [4.9041, 52.3676] }, signal: AbortSignal.timeout(5000) });} catch (error) { if (!(error instanceof SDKError)) throw error;
switch (error.code) { case 'ABORTED': console.error('Search timed out'); break; case 'INVALID_CREDENTIALS': console.error('Check the API key'); break; case 'INSUFFICIENT_PERMISSIONS': console.error(`Check the APIs and domains the key allows: ${error.message}`); break; case 'RATE_LIMITED': console.error('Rate limited, even after retrying'); break; case 'NETWORK_NOT_AVAILABLE': console.error('No connection'); break; default: console.error(`${error.service} failed (${error.code}, SDK ${error.sdkVersion}): ${error.message}`); }}- Branch on
code, not onmessage. The codes are the stable part.sdkErrorCodes(core) lists them all, under the names the TomTom Navigation SDK gives the same failures. canRetryistruewhen the failure clears on its own. That coversRATE_LIMITED,LIVE_SERVICE_NOT_AVAILABLEandNETWORK_NOT_AVAILABLE, where the same call is likely to succeed later. On any other code, retrying fails the same way.- A
429that rejects has already been retried for as long asretryallows. - On a
429, the SDK replaces the API’s message with a standard one. - The map reports its own failures with the same codes. See Map errors.
The code below geocodes a batch of addresses in parallel under one deadline, and reads each failure from Promise.allSettled by its code and canRetry. Swap the latitude-first position for [139.76, 35.68] and that address resolves:
A key refused access
A 401 and a 403 are two different problems. INVALID_CREDENTIALS means the API doesn’t know the key. INSUFFICIENT_PERMISSIONS means the key is valid but may not make this request. apiErrorCode names the cause of a 403:
apiErrorCode | Cause | Fix |
|---|---|---|
'Forbidden' | The API is not enabled for the key | Enable it for the key, or for a Private Preview API, request access |
'InvalidReferer' | The key’s domain restriction refused the page | Add the page’s domain to the key, or set a fixed referrer from your proxy |
A Private Preview API, such as Calculate Reachable Range, needs access granted to the key. The Freemium and Pay As You Grow plans don’t include it. When such an API answers 'Forbidden', or a proxy answers a 403 with no body, the error says so in two places:
- The message ends with the API that needs access, a link to the Private Preview terms and where to request the access.
privatePreviewAccesscarries the same information as data,{ product, requestAccessURL }, so you can show your own message:
import { calculateReachableRange, SDKServiceError } from '@tomtom-org/maps-sdk/services';
try { await calculateReachableRange({ origin: [4.9041, 52.3676], budget: { type: 'timeMinutes', value: 30 } });} catch (error) { if (error instanceof SDKServiceError && error.privatePreviewAccess) { const { product, requestAccessURL } = error.privatePreviewAccess; showMessage(`${product} needs access on your API key: ${requestAccessURL}`); } else throw error;}Related guides and examples
Related guides
- Services quickstart
- Global configuration: the settings every call inherits
- Places services quickstart
- Routing services quickstart
- Proxying your API calls: keeping the API key server-side
Related examples
- Search playground: one
createLatestRequestbehind every keystroke, pick and page - Reverse geocoding playground: a click lookup that retires the previous click’s request
- Node.js geocoding: service calls on a server
- Node.js batch geocoding: a batch whose failures are told apart by
code, under one deadline