← Back to Blog

Migrating to the geometry operators — the load() call nine of them need, and the error that names nothing

A card of geometry primitives on the left, holding a pentagon, a point, a polyline and a dashed buffer ring, fans out to six rounded operator tiles each badged with a small ring; the tiles feed a card where a dashed spinner downloads into a blue cube, then a solid arrow runs through a completed buffer tile to a map panel showing a cyan buffer disc under a blue check mark, while a dashed gray branch from the cube card passes through a crossed out node to a pale message card whose exclamation icon sits beside two blank gray text bars

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():

GroupOperators 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 functionReplacement
geodesicAreageodeticAreaOperator
geodesicDensifygeodeticDensifyOperator
geodesicLengthgeodeticLengthOperator
planarAreaareaOperator
planarLengthlengthOperator
intersectintersectionOperator
intersectLinesToPointsthe executeMany() method of intersectionOperator
isSimplethe isSimple() method of simplifyOperator
nearestCoordinatethe getNearestCoordinate() method of proximityOperator
nearestVertexthe getNearestVertex() method of proximityOperator
nearestVerticesthe getNearestVertices() method of proximityOperator
rotatethe rotate() method of Transformation, with affineTransformOperator
flipHorizontalthe flipY() method of Transformation, with affineTransformOperator
flipVerticalthe 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

  1. Take each replacement from the reference, not from the pattern. The deprecation note on every geometryEngine function except extendedSpatialReferenceInfo names its replacement, and the table above shows how often the pattern guesses wrong.
  2. Find the gated operators. Any geodesic or geodetic operator, plus projectOperator and shapePreservingProjectOperator. Each one your code uses needs its own awaited load() before its first call. The guarded and unguarded forms both work.
  3. Leave the other 43 alone. They have no load() to call.
  4. Read “is not a function” from an operator as a missing load() until you have ruled that out.
  5. 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 geodesicBufferOperator instead.
  6. Treat a startup that never finishes as a module path problem. A misspelled import hangs instead of failing.
  7. 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.
If you are not the one writing the code. 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

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