Migrating to the geometry operators — the load() call nine of them need, and the error that names nothing
geometryEngine has been deprecated since version
4.32 of the ArcGIS Maps SDK for JavaScript, and its reference page says what to do in one
line: “Use geometry operators instead.” All but one of its functions name the operator
that replaces it, so the migration looks like a find-and-replace. It is not one. Nine of the
52 geometry operators will not run until you call their load()
method and wait for it to finish. The other 43 have no load()
method at all. Skip it on geodesicBufferOperator and the SDK throws
TypeError: a is not a function, an error that names neither the
operator nor the call you left out.
This post covers which nine, the rule that predicts them, what
load() actually downloads, and two traps that sit beside the
migration: a planar buffer that quietly measures in degrees, and a misspelled module name
that hangs instead of failing. Everything here was checked against version 5.1.24, published
to npm on September 11, 2026 as the latest release. Every code sample below is an excerpt
from a test page we ran in headless Chromium against the SDK’s 5.1 CDN build, and the
outputs quoted are what those pages recorded.
What still works today
Your existing geometryEngine calls keep working at 5.1.24: the
deprecated module still loads and still returns geometry. Our test pages load the SDK with
<script type="module" src="https://js.arcgis.com/5.1/"></script>,
run each snippet inside a <script type="module"> block, and
import modules through $arcgis.import(). That
/5.1/ URL is an alias for the latest 5.1 patch, and every
measurement below comes from 5.1.24; production code can pin
https://js.arcgis.com/5.1.24/, trading automatic updates, which
Esri warns “might impact your application in unintended ways,” for patches that
arrive only when someone edits the URL.
This is the old call, with no load() and nothing to wait
for:
const Point = await $arcgis.import("@arcgis/core/geometry/Point.js"); const geometryEngine = await $arcgis.import("@arcgis/core/geometry/geometryEngine.js"); const geodesicBufferOperator = await $arcgis.import("@arcgis/core/geometry/operators/geodesicBufferOperator.js"); const pt = new Point({ longitude: -105.0, latitude: 40.0 }); // ---- the OLD, deprecated call. No load(), no await, nothing. ---- const poly = geometryEngine.geodesicBuffer(pt, 1000, "meters");
That returned a polygon with 75 vertices and no error. The only sign of the deprecation
was a console warning the SDK logged when the module was imported:
DEPRECATED - Module: @arcgis/core/geometry/geometryEngine,
followed by Version: 4.32 and a link to more details.
The replacements are not experimental either. Of the 52 operator modules in 5.1.24, 48 first shipped at 4.31, and none is marked beta in the package’s type declarations. And once a migrated call is written correctly, it returns the same geometry: the operator version of the buffer above also had 75 vertices, and the geodetic areas of the old and new polygons agreed to a ratio of 1.0000000000050373.
The rename that throws
Here is the same buffer written the way the deprecation note suggests, with the operator it names swapped in for the last line above:
// ---- the NEW call the docs tell you to use, written as a straight rename. ---- const poly = geodesicBufferOperator.execute(pt, 1000, { unit: "meters" });
It does not return a polygon. It does not return null, and it
does not return a wrong answer. It throws
TypeError: a is not a function.
That a is a single-letter local variable name inside the
SDK’s built JavaScript, and it differs from module to module. The same mistake on
geodeticLengthOperator threw
TypeError: i is not a function, and on
projectOperator it threw
TypeError: s is not a function. None of the three messages
mentions load() or the operator, so there is nothing useful to
search for, and the same bug arrives under a different letter depending on which operator
you hit. That makes this the single most useful rule for the migration: when an
operator’s execute() throws “is not a function,”
check whether its load() ran before you look at anything
else.
The error is thrown to whatever code called execute(), so
whether it ever reaches a console depends on how that code handles errors. There is a
second, quieter symptom you can check directly: the operator’s
supportsCurves property. The reference says: “The value is
null or undefined until the operator is loaded, then it will always be true.” That is
what we measured on geodesicBufferOperator:
null before load(),
true after.
The fix, again in place of that last line, is one guarded
await, in the same form the operator’s reference page uses in
its example:
// ---- the NEW call written correctly. ---- if (!geodesicBufferOperator.isLoaded()) { await geodesicBufferOperator.load(); } const poly = geodesicBufferOperator.execute(pt, 1000, { unit: "meters" });
That returned the polygon. Only load() is asynchronous here;
execute() hands back its geometry directly, as it does in every
sample in this post. Esri’s own geodesic buffer sample uses a shorter form,
await geodesicBufferOperator.load(); with no
isLoaded() check, and we measured both as safe. Four
load() calls fired at the same operator at once all fulfilled. A
second load() on an operator that was already loaded resolved
without fetching anything again, and the buffer computed afterwards was unchanged. The two
documented forms differ in style, not in effect.
Which nine, and the rule that predicts them
These are the nine operators that export load():
| Group | Operators that need load() |
|---|---|
| Geodesic | geodesicBufferOperator,
geodesicProximityOperator |
| Geodetic | geodeticAreaOperator,
geodeticDensifyOperator,
geodeticDistanceOperator,
geodeticLengthOperator,
geodeticUtilsOperator |
| Projection | projectOperator,
shapePreservingProjectOperator |
So the rule is short. Every operator whose name starts with
geodesic or geodetic, plus the two
projection operators, needs load(), and no other operator has
one. We checked that rule against all 52 operator modules in two independent ways:
by reading the type declarations shipped in the 5.1.24 package, and by importing every module
in a browser and testing it for a load function. Both methods gave
the same nine, with no exceptions. All nine also expose
isLoaded(), and it returned false for
every one of them on a fresh page.
The other 43 are not optional versions of the same gate. They have no gate.
bufferOperator exports exactly three members,
execute, executeMany and
supportsCurves, and a planar buffer ran correctly on a page that
never called load() anywhere. The reference pages reflect the
split. The geodesicBufferOperator and
projectOperator pages both carry the note “Verify that
isLoaded() returns true before using this module. Use
load() to load this module’s dependencies.” The word isLoaded does not appear anywhere
on the bufferOperator page.
One more property of the gate matters if you plan to load everything once at startup:
load() is per operator. After we loaded
geodesicBufferOperator, the other eight still reported
isLoaded() as false, and calling
geodeticLengthOperator.execute() at that moment threw
TypeError: i is not a function. After its own
load(), the same call returned 85,393.409 meters for one degree
of longitude along the 40th parallel, the 85.4 kilometers expected there. Every gated
operator your code uses needs its own load().
What load() downloads
Esri’s reference describes load() in two sentences:
“Loads this module’s dependencies. This method must be called first if
isLoaded returns false.” Neither says what those
dependencies are, so we measured them. We recorded every request a test page made while its
load() call was running, then compared those requests with the
ones made by a page that ran only a planar buffer. On a fresh page,
geodesicBufferOperator.load() requested 19 files from the CDN, and
the planar page fetched 16 of them too. The three the planar page never fetched were
esri/geometry/support/pe-wasm.wasm, the projection engine as a
.wasm binary; a JavaScript chunk named pe-wasm.js; and a chunk
named OperatorGeodesicBuffer.js.
projectOperator.load() requested 18 files, and the ones the planar
page never fetched were the same two pe-wasm files and a chunk
named projectionTransformation.js.
geodeticLengthOperator.load() requested 21, and the ones the
planar page never fetched were the same two plus chunks named
OperatorGeodeticLength.js and
OperatorShapePreservingLength.js.
The CDN served every one of those files compressed
(Content-Encoding: br). The .wasm file
is 2,844,903 bytes uncompressed, and what actually crosses the network is 660,623 bytes.
Counting only the files the planar page never fetched, a gated operator added 676,295 bytes
on the wire for geodesicBufferOperator, 670,872 for
geodeticLengthOperator and 668,822 for
projectOperator, almost all of it the projection engine. Counting
everything load() requested on those three pages, none of which
used a planar operator, the totals were 957,789, 952,397 and 949,916 bytes. Every file arrived
with Cache-Control: max-age=36000,s-maxage=31557600, which lets a
browser reuse it for ten hours, so repeat visits within that window do not download it again.
These figures are our measurement of the 5.1.24 CDN build; Esri’s documentation does not
state them.
The projection engine download is the real cost of the gated operators, and it is paid in
bandwidth. In every test page we ran, no request went to any Esri host other than the
js.arcgis.com CDN, and no page set an API key or carried a token.
A page that uses only planar operators never downloads the projection engine at all.
The planar trap worth fixing while you are there
A migration puts every geometry call in your code in front of you, which makes it a good
moment to catch a mistake that has nothing to do with load(). A
planar operator such as bufferOperator takes its distance in the
units of the input geometry’s spatial reference, and for the WGS84 point used above,
those units are degrees:
const bufferOperator = await $arcgis.import("@arcgis/core/geometry/operators/bufferOperator.js"); // (a) Planar buffer of "1000" on a geographic geometry. No load() is required, no error // is raised, and the number is interpreted in the spatial reference's own unit: DEGREES. const planar = bufferOperator.execute(pt, 1000);
That returned a valid polygon, with no error and no warning, 2,000 degrees wide — more than five times the 360 degrees of longitude on Earth. The geodesic buffer of the same point and the same number was 0.0234 degrees wide, so the planar result was 85,470 times wider than the intended one.
Now add the option you would naturally reach for:
const r = bufferOperator.execute(pt, 1000, { unit: "meters" });
This time the operator throws
Error: Unable to convert from a linear unit to an angular unit.
That behavior is documented. The reference says of the unit
option: “An error will be thrown if this is set for Geographic Coordinate
Systems.” The contrast with the gate is worth noticing: the unit error explains itself,
and the missing-load() error does not. For a buffer measured in
meters around a longitude and latitude, use geodesicBufferOperator,
loaded.
None of this is new with the operators. geometryEngine.buffer
is documented as creating “planar (or Euclidean) buffer polygons,” with the same
behavior on geographic coordinates. It is a long-standing trap, and the migration is simply a
convenient time to go looking for it. The geometryEngine.buffer
entry also says when to use the geodesic buffer instead: “As a general rule, use
geodesicBuffer() if the input geometries have a spatial reference
of either WGS84 (wkid: 4326) or Web Mercator.”
The names a find-and-replace gets wrong
The deprecation notes are precise, but they do not follow a single pattern. Several
geometryEngine functions are not replaced by the old name with
Operator added. From the geometryEngine
reference:
geometryEngine function | Replacement |
|---|---|
geodesicArea | geodeticAreaOperator |
geodesicDensify | geodeticDensifyOperator |
geodesicLength | geodeticLengthOperator |
planarArea | areaOperator |
planarLength | lengthOperator |
intersect | intersectionOperator |
intersectLinesToPoints | the executeMany() method of intersectionOperator |
isSimple | the isSimple() method of simplifyOperator |
nearestCoordinate | the getNearestCoordinate() method of proximityOperator |
nearestVertex | the getNearestVertex() method of proximityOperator |
nearestVertices | the getNearestVertices() method of proximityOperator |
rotate | the rotate() method of Transformation, with affineTransformOperator |
flipHorizontal | the flipY() method of Transformation, with affineTransformOperator |
flipVertical | the flipX() method of Transformation, with affineTransformOperator |
Three of the four old geodesic functions became
geodetic operators, while geodesicBuffer
became geodesicBufferOperator. So the name the pattern predicts,
geodeticBufferOperator, does not exist. Requesting it from the
CDN returns 404; geodesicBufferOperator.js returns 200.
What happens when you import a name that does not exist is worse than an error.
In our tests, $arcgis.import() of a module that does not
exist neither resolved nor rejected. We raced four misspelled operator names against
a six-second timer, and all four simply hung. A test page that imported
geodeticBufferOperator had still not finished initializing when
it timed out after 30 seconds, and it raised no page error. In the console, the
SDK’s module loader logged this error:
Error: scriptError: https://js.arcgis.com/5.1/esri/geometry/operators/geodeticBufferOperator.js.
A try/catch around that import has
nothing to catch. If an application stops finishing its startup right after a migration,
check the module paths first.
A migration checklist
- Take each replacement from the reference, not from the pattern. The
deprecation note on every
geometryEnginefunction exceptextendedSpatialReferenceInfonames its replacement, and the table above shows how often the pattern guesses wrong. - Find the gated operators. Any
geodesicorgeodeticoperator, plusprojectOperatorandshapePreservingProjectOperator. Each one your code uses needs its own awaitedload()before its first call. The guarded and unguarded forms both work. - Leave the other 43 alone. They have no
load()to call. - Read “is not a function” from an operator as a missing
load()until you have ruled that out. - Look at every planar buffer that runs on WGS84 or Web Mercator
geometry. Esri’s rule for both is a geodesic buffer, and on WGS84 longitude
and latitude a planar buffer’s distance is in degrees. Use
geodesicBufferOperatorinstead. - Treat a startup that never finishes as a module path problem. A misspelled import hangs instead of failing.
- Expect a larger download on pages that use a gated operator: in our 5.1.24 tests, 668,822 to 676,295 bytes on the wire that a page using only a planar operator never downloaded, almost all of it the projection engine, reused by the browser for ten hours.
geometryEngine still works at 5.1.24, so this is planned work,
not an outage. Do not read that as a rule for everything deprecated alongside it:
geometryEngineAsync and projection,
deprecated in the same 4.32 release, were removed at 5.0. The migration is small for each
call, but it is not a search-and-replace. Its two failure modes are hard to trace. One is an
error that names nothing. The other is a misspelled module name, which leaves the application
hanging at startup: the import never finishes, so the application’s own error handling
has nothing to catch, although in our tests the browser console did show an error naming the
missing file. In our tests the operators needed no API key and made no request to any Esri service.
The cost that does exist is bandwidth: in our tests, a geodesic, geodetic or projection
operator added 668,822 to 676,295 bytes of downloads that a page using only a planar operator
never made, almost all of it the projection engine, and a browser can reuse those files for
ten hours after downloading them. Three questions to put to
whoever owns the code: which calls use one of those operators, does each one await its own
load(), and does any planar buffer run on WGS84 or Web Mercator
geometry?What we did not test
Of the 43 operators without a gate, we exercised two, bufferOperator
and areaOperator. For the other 41 we checked only that they
export no load(). We made no measurements at volume, so this post
makes no claim about throughput, executeMany, or work on the main
thread. The bufferOperator reference links a sample titled
“Geometry operator - using a worker for analysis,” but that link currently redirects
to Esri’s “Introduction to geometry operators” guide, listed below. We ran no
worker code for this post.
The short version: at 5.1.24 the move off geometryEngine
produced the same geometry in our tests, as long as it is treated as nine operators with a
gate and 43 without one. Write the awaited load() for each gated
operator you use, and the error that names nothing never gets the chance to appear.
References
geometryEnginereference (the 4.32 deprecation and the replacement named on each function exceptextendedSpatialReferenceInfo)geodesicBufferOperatorreference (load(),isLoaded(),supportsCurvesand the verify-before-use note)projectOperatorreference (the projection operator that carries the same gate)bufferOperatorreference (the planar buffer, its unit option and the geographic coordinate system error)- Sample: geometry operator geodesic buffers (Esri’s sample, which awaits
load()without anisLoaded()check) - Introduction to geometry operators (where the
bufferOperatorreference’s worker sample link currently redirects; further reading) - Get started with the Maps SDK (the CDN script tag and
$arcgis.import()form every sample here uses) - Release notes for 5.0 (the
MAJOR.MINORCDN alias, and the removal ofgeometryEngineAsyncandprojection, both deprecated at 4.32)
Moving an application off geometryEngine?
We build and review ArcGIS Maps SDK for JavaScript applications on the current release. Book a free intro call and we will go through your geometry code with you: which calls reach a gated operator, which planar buffers run on WGS84 or Web Mercator geometry, and what the migration will actually involve.
Book a free intro call