CesiumJS Ready Promise Deprecation / API Changes

Hi all,

The CesiumJS squad is looking to deprecate readyPromise and related patterns throughout the API, and we’d like to know if you have any feedback on these changes.

Why get rid of readyPromise?

The readyPromise pattern was established in many part of the cesium API, such as 3D Tiles, ImageryProvider, and TerrainProviders, to handle asynchronous operation such as making request to load metadata before all properties of an object can become available. These asynchronous operations are initiated on object creation and aggregated into a promise.

A common place this pops up is with Cesium3DTilesets– It’s required to await the readyPromise property before the bounding sphere can be accessed.

tileset.readyPromise
  .then(function (tileset) {
    viewer.zoomTo(
      tileset,
      new Cesium.HeadingPitchRange(
        0.5,
        -0.2,
        tileset.boundingSphere.radius * 4.0
      )
    );
  })
  .catch(function (error) {
    console.log(error);
  });

However, this has been a problematic part of the CesiumJS API that does not match modern promise best practices.

  • The background asynchronous nature can cause race conditions, especially if they do not properly handle destroyed objects. This has been coming up often in tests which assume these background operation can be ignored, leading to “random” CI failures.
  • The existing API can cause confusion around how to handle errors.
  • When readyPromises are unhandled, in many modern systems, a failed promise can lead to a critical error.

How will the API change?

The plan is to deprecate readyPromise properties throughout the API starting with release 1.104. As this affects high-touch areas of the API like imagery providers, terrain providers, and 3D Tiles, we’ll remove the old code paths after three releases.

The new implementation moves asynchronous operations out of synchronous constructors and into async factory functions, which perform operations like loading necessary metadata up front, returning a promise that resolves to the created 3D tileset or provider. The created object can then be assumed fully ready, and all properties can be accessed immediately (such as Cesium3DTileset.boundingSphere). 3D tilesets or providers will still make requests for new tiles as changes to the camera and scene occur and is handled internally, the success or failure of which are exposed via events.

So what does this look like?

A few examples of this are as follows:

3D Tiles

Before
const tileset = scene.primitives.add(
  new Cesium.Cesium3DTileset({
    url: Cesium.IonResource.fromAssetId(1240402),
  })
);

tileset.readyPromise
  .then(function (tileset) {
    // Wait to reference tileset properties
    viewer.zoomTo(
      tileset,
      new Cesium.HeadingPitchRange(
        0.5,
        -0.2,
        tileset.boundingSphere.radius * 4.0
      )
    );
  })
  .catch(function (error) {
    // Handle errors
    console.log(`There was an error while creating the 3D tileset. ${error}`);
  });
After
try {
  const tileset = await Cesium3DTileset.fromUrl(Cesium.IonResource.fromAssetId(1240402));
  scene.primitives.add(tileset);
  // Immediately reference tileset properties
  viewer.zoomTo(
      tileset,
      new Cesium.HeadingPitchRange(
        0.5,
        -0.2,
        tileset.boundingSphere.radius * 4.0
      )
    );
} catch (error) {
  // Handle errors
    console.log(`There was an error while creating the 3D tileset. ${error}`);
}

Imagery

Before
const imageryProvider = Cesium.TileMapServiceImageryProvider({
    url: Cesium.buildModuleUrl("Assets/Textures/NaturalEarthII")
});

imageryProvider.readyPromise.then(() => {
    // Wait to reference provider properties
    const tilingScheme = imageryProvider.tilingScheme;
}).catch(error => {
    // Handle errors
    console.log(`There was an error while creating the imagery layer. ${error}`);
});


After
try {
  const imageryProvider = await Cesium.TileMapServiceImageryProvider.fromUrl(
    Cesium.buildModuleUrl("Assets/Textures/NaturalEarthII")
   );
   const imageryLayer = new ImageryLayer(imageryProvider);
   scene.imageryLayers.add(imageryLayer);

   // Immediately access provider properties
  const tilingScheme = imageryProvider.tilingScheme;
} catch (error) {
  // Handle error
  console.log(`There was an error while creating the imagery layer. ${error}`);
}

Alternatively, you can create an ImageryLayer from the async factory function and deal with any errors via events. This is convenient for synchronously configuring the Cesium Viewer or CesiumWidget. By default, errors will be caught and logged to the console, but this behavior can also be overridden by subscribing to the errorEvent.

const baseLayer = Cesium.ImageryLayer.fromProviderAsync(
    Cesium.TileMapServiceImageryProvider.fromUrl(
      Cesium.buildModuleUrl("Assets/Textures/NaturalEarthII")
    )
);

baseLayer.errorEvent.addEventListener(error => {
    console.log(`There was an error creating the base imagery layer. ${error}`);
});

const viewer = new Cesium.Viewer("cesiumContainer", {
    baseLayer: baseLayer,
    baseLayerPicker: false,
});

Terrain

Before
const worldTerrainProvider = Cesium.createWorldTerrain();
worldTerrainProvider.readyPromise.catch(error => {
      // Handle error
      console.log(`There was an error while creating terrain. ${error}`);
    });
After
try {
    const worldTerrainProvider = await Cesium.createWorldTerrainAsync();
    viewer.terrainProvider = worldTerrainProvider;
} catch (error) {
    console.log(`Failed to load terrain. ${error}`);
}

Alternatively, there is now a Terrain class available to help handle the asynchronous operation and expose them as events. This is convenient for synchronously configuring the Cesium Viewer or CesiumWidget. By default, errors will be caught and logged to the console, but this behavior can also be overridden by subscribing to the errorEvent.

const viewer = new Cesium.Viewer("cesiumContainer", {
    terrain: Cesium.Terrain.fromWorldTerrain(),
});

Please give us your feedback!

As this will be a fairly major change to some of the core CesiumJS APIs, please let us know if you have any questions, concerns, or feedback.

Thanks!

@Gabby_Getz Hi Gabby, thanks for the post, this will help us to migrate our code.

I have another question, and I wonder if you could help?

  1. We have been using Cesium v1.96 for displaying New York City b3dm dataset, which worked nicely.
  2. Then we upgraded it to v1.106, an error started to show up:
    “RuntimeError: Fragment shader failed to compile. Compile log: ERROR: 0:236: ‘=’ : dimension mismatch ERROR: 0:236: ‘assign’ : cannot convert from ‘highp 2-component vector of float’ to ‘highp 3-component vector of float’”.
  3. Then I used the latest v1.113 to display the data in a standalone application (before I replace this imageryProvider.readyPromise logic), the New York City b3dm still could not be displayed.

Could you get a moment to do a quick test to see how to make this dataset work?

<html lang="en">
    <head>
        <!--from repo server-->
        <script src="https://cesium.com/downloads/cesiumjs/releases/1.113/Build/Cesium/Cesium.js"></script>
        <link href="https://cesium.com/downloads/cesiumjs/releases/1.113/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
    </head>
    <body>
        <div id="cesiumContainer"   style="position:absolute; width:80%; height:80%"></div>
        <script>
            const viewer = new Cesium.Viewer('cesiumContainer');
            try {
                const tileset = Cesium.Cesium3DTileset.fromUrl('upgradedNY/tileset.json');
                tileset.then(function(tileset){
                    viewer.scene.primitives.add(tileset);
                    viewer.zoomTo(
                        tileset,
                        new Cesium.HeadingPitchRange(0.5,-0.2,tileset.boundingSphere.radius * 4.0)
                    );
                });
            } catch (error) {
              console.log(error)
            }
           

            // v113 error: TypeError: Cannot read properties of undefined (reading 'updateTransform')
            //viewer.zoomTo(tileset, new Cesium.HeadingPitchRange(0, -0.5, 0));

            // Remove default base layer
            viewer.imageryLayers.remove(viewer.imageryLayers.get(0));

            var wmts = new Cesium.WebMapTileServiceImageryProvider({
                url : 'https://services.arcgisonline.com/arcgis/rest/services/World_Topo_Map/MapServer/WMTS/',
                layer : 'World_Topo_Map',
                style : 'default',
                tileMatrixSetID : 'default028mm',
            });
            
            var arcgis = viewer.imageryLayers.addImageryProvider(wmts);
            
            arcgis.alpha = 1.0; // 0.0 is transparent.  1.0 is opaque.
            arcgis.brightness = 1.0; // > 1.0 increases brightness.  < 1.0 decreases.
        </script>
    </body>
</html>

Thank you!

Hi there,

One other major change was that in version 1.102, we moved to WebGL 2 by default. Does your dataset work if you request as WebGL 1 context with the following code?

const viewer = new Viewer("cesiumContainer", {
  contextOptions: {
    requestWebgl1: true,
  },
});

@Gabby_Getz , thank you for your response. Please disregard my above questions, since @javagl (Marco Hutter) from 3d-tiles-tools is helping me to fix the dataset issue (Upgraded New York City b3dm tiles, but data not rendered using CesiumJS v1.113 · Issue #95 · CesiumGS/3d-tiles-tools · GitHub). Thanks!

Hi @Gabby_Getz , where upgrading our App into this newer API by replace/removing the readyPromise, I ran into an issue: after adding a Cesium3DTileser, the entity.id is ‘undefined’:
1 of 4:

2 of 4:


3 of 4:

4 of 4: the error message:

All of the above was happening after the below code has fulfilled the .then promise:

Additional info.
1: Before this migration, we were on V1.106.1, and this feature was working fine.
2: It works on v1.113 in my Stand-alone POC test case.

I debugged a bit more by comparing my standalone test with the one in my App, and found that in the updateZoomTarget function in Viewer.js, the ‘target’ is a Cesium3DTileset, in the standalone, which works; while in my App, the ‘target’ ends up as an array of Cesium3DTileset (as shown below):

Please let me know which area I need to look into to fix the issue.

Thank you in advance.

HI @honglzhu, would you be able to provide a minimal code example which replicates the error you’re seeing?

Hi @Gabby_Getz , Good news! After I posted the messages above, I noticed that there was a newer CesiumJS release, i.e., v1.114. Then I went ahead and used v1.114, instead of v1.113:
image.

After recompiling the package, all the issues are gone and everything works! So the new release must have enhanced something to allow my App’s use-case work.

Thank you the same for getting back to me. By the way, your sample code are very helpful in this migration (from v1.106 to v1.114).

Thank you again!

@Gabby_Getz fyi: I found a new issue in version 1.114 and created a ticket: Version 1.114 Zoom view not working as expected when zoom-in · Issue #11822 · CesiumGS/cesium · GitHub. Thanks.

As mentioned in Version 1.114 Zoom view not working as expected when zoom-in · Issue #11822 · CesiumGS/cesium · GitHub : When you create the tileset with

const tileset = await Cesium.Cesium3DTileset.fromUrl('nyc_v2/tileset.json', {
    disableCollision: true
});

it should restore the behavior from 1.113, maybe that’s a suitable workaround for the zooming issue for now.

@Marco13 Thank you! It indeed helped!

@Gabby_Getz you removed readyPromise, but how can I be notified when a primitive has finished rendering ? or the worker has finished building the points for rendering ?

Is it possible to get a notification when the primitivecollection has finished rendering all it’s primitive ?

thanks you,

Hi @Mickael ,
Thanks for your post.

When we removed readyPromise we provided replacement functionality to cover the use case you are asking about to provide notification when a primitive has loaded.

Please check out this link in the CesiumJS changelog:

I think this sample code from the changelog will be particularly helpful to you.

try {
  const model = await Cesium.Model.fromGltfAsync({
    url: "../../SampleData/models/CesiumMan/Cesium_Man.glb",
  });
  viewer.scene.primitives.add(model);
  model.readyEvent.addEventListener(() => {
    // model is ready for rendering
  });
} catch (error) {
  console.log(`Failed to load model. ${error}`);
}

Please let me know if this is not what you are looking for or if you have additional questions.
Best,
Luke

@Luke_McKinstry
Thanks but I’m using Geometry primitive :stuck_out_tongue:

I need to find a way to send thousands lines, and from my benchmark, it’s not good to create just one primitive because it takes a lot of time to combine. But using too much primitive is not good either , my tasks is not easy .

That why, I’m trying to send geometry primitive by packets , and when the first packet has finished combined, i send an another packet of geometry :stuck_out_tongue:

And sending primtive by packets is better, because the user can see a render starting earlier than a big primitive !

most of the time I’m using PolylineGeometry.

i hate billboard because they are synchronous only, so I have to send them at the end …

you can find my logs there :

Case of few primitive but thousands (more than 100 000) polyline geometry with async :

19:35:58.568 msg rcv start
19:36:00.250 msg json parsed in 0.935
19:36:00.250 addTransaction begin
19:36:02.790 primitive READY
… repeated
19:36:05.048 primitive READY
19:36:05.048 addTransaction end
19:36:05.049 msg rcv end in 5.734
19:36:05.049 msg rcv start
19:36:05.049 msg json parsed in 0.000
19:36:05.049 invokeSignalCallbacks end in 0.000
19:36:05.050 signalEmitted end in 0.000
19:36:05.050 handleSignal end in 0.001
19:36:05.050 msg rcv end in 0.001
19:36:05.050 msg rcv start
19:36:05.050 msg json parsed in 0.000
19:36:05.050 invokeSignalCallbacks end in 0.000
19:36:05.051 signalEmitted end in 0.000
19:36:05.051 handleSignal end in 0.000
19:36:05.051 msg rcv end in 0.000
19:36:05.971 primitive CREATING
19:36:07.370 msg rcv start
19:36:07.821 msg json parsed in 0.182
19:36:07.821 addTransaction begin
19:36:10.169 primitive READY
19:36:10.180 primitive READY
19:36:10.187 primitive READY
19:36:10.192 primitive READY
19:36:10.200 primitive READY
19:36:10.205 primitive READY
19:36:10.210 primitive READY
19:36:10.218 primitive READY
19:36:10.223 primitive READY
19:36:10.227 primitive READY
19:36:10.232 primitive READY
19:36:10.240 primitive READY
19:36:10.245 primitive READY
19:36:10.249 primitive READY
19:36:10.254 primitive READY
19:36:10.262 primitive READY
19:36:10.267 primitive READY
19:36:10.272 primitive READY
19:36:10.277 primitive READY
19:36:10.285 primitive READY
19:36:10.290 primitive READY
19:36:10.294 primitive READY
19:36:10.302 primitive READY
19:36:10.305 primitive READY
19:36:10.310 primitive READY
19:36:10.312 primitive READY
19:36:10.312 addTransaction end
19:36:10.312 msg rcv end in 2.673
19:36:18.969 primitive CREATED
19:36:18.970 msg rcv start
19:36:18.971 msg json parsed in 0.000
19:36:18.971 invokeSignalCallbacks end in 0.000
19:36:18.972 propertyUpdate end in 0.000
19:36:18.972 handlePropertyUpdate end in 0.001
19:36:18.972 msg rcv end in 0.002
19:36:19.071 primitive promise set state=combining
19:36:19.071 primitive COMBINING
19:36:44.554 primitive COMBINED
19:36:46.830 primitive COMPLETE

with a lot of primitive :

20:04:29.509 msg rcv start
20:04:29.510 msg json parsed in 0.000
20:04:29.510 invokeSignalCallbacks end in 0.000
20:04:29.510 propertyUpdate end in 0.000
20:04:29.511 handlePropertyUpdate end in 0.000
20:04:29.511 msg rcv end in 0.001
20:04:35.745 msg rcv start
20:04:37.518 msg json parsed in 1.029
main.a689a849a5162369e2e3.js:4329 20:04:37.518 addTransaction begin
20:04:37.687 primitive READY
… repeated
20:04:42.198 primitive READY
main.a689a849a5162369e2e3.js:4329 20:04:42.198 addTransaction end
20:04:42.198 msg rcv end in 5.709
20:04:42.332 primitive CREATING
20:04:42.421 primitive CREATING
20:04:42.500 primitive CREATING
20:04:42.560 primitive CREATING
20:04:42.623 primitive CREATING
20:04:42.684 primitive CREATING
20:04:42.750 primitive CREATING
20:04:42.811 primitive CREATING
20:04:42.876 primitive CREATING
20:04:42.940 primitive CREATING
20:04:43.020 primitive CREATING
20:04:43.159 primitive CREATING
20:04:43.276 primitive CREATING
20:04:43.397 primitive CREATING
20:04:43.443 primitive CREATING
20:04:43.928 msg rcv start
20:04:43.928 msg json parsed in 0.000
20:04:43.929 invokeSignalCallbacks end in 0.000
20:04:43.929 signalEmitted end in 0.001
20:04:43.929 handleSignal end in 0.001
20:04:43.929 msg rcv end in 0.001
20:04:43.930 msg rcv start
20:04:43.930 msg json parsed in 0.000
20:04:43.931 invokeSignalCallbacks end in 0.000
20:04:43.931 signalEmitted end in 0.000
20:04:43.931 handleSignal end in 0.000
20:04:43.931 msg rcv end in 0.001
20:04:44.453 primitive CREATED
20:04:44.717 primitive promise set state=combining
20:04:44.723 primitive COMBINING
20:04:45.504 primitive CREATED
20:04:45.507 primitive CREATED
20:04:45.559 primitive promise set state=combining
20:04:45.559 primitive COMBINING
20:04:45.579 primitive promise set state=combining
20:04:45.579 primitive COMBINING
20:04:45.663 primitive CREATED
20:04:45.672 primitive promise set state=combining
20:04:45.673 primitive COMBINING
20:04:46.075 primitive CREATED
20:04:46.283 primitive promise set state=combining
20:04:46.283 primitive COMBINING
20:04:46.583 primitive CREATED
20:04:46.704 primitive CREATED
20:04:46.709 primitive promise set state=combining
20:04:46.709 primitive COMBINING
20:04:46.712 primitive promise set state=combining
20:04:46.713 primitive COMBINING
20:04:46.726 primitive CREATED
20:04:46.727 primitive CREATED
20:04:46.738 primitive promise set state=combining
20:04:46.738 primitive COMBINING
20:04:46.742 primitive promise set state=combining
20:04:46.743 primitive COMBINING
20:04:46.778 primitive CREATED
20:04:46.803 primitive promise set state=combining
20:04:46.803 primitive COMBINING
20:04:46.813 primitive CREATED
20:04:46.833 msg rcv start
20:04:47.119 msg json parsed in 0.193
main.a689a849a5162369e2e3.js:4329 20:04:47.119 addTransaction begin
20:04:49.045 primitive READY
20:04:49.059 primitive READY
20:04:49.067 primitive READY
20:04:49.072 primitive READY
20:04:49.081 primitive READY
20:04:49.086 primitive READY
20:04:49.091 primitive READY
20:04:49.096 primitive READY
20:04:49.104 primitive READY
20:04:49.109 primitive READY
20:04:49.114 primitive READY
20:04:49.119 primitive READY
20:04:49.128 primitive READY
20:04:49.133 primitive READY
20:04:49.138 primitive READY
20:04:49.146 primitive READY
20:04:49.151 primitive READY
20:04:49.156 primitive READY
20:04:49.161 primitive READY
20:04:49.170 primitive READY
20:04:49.174 primitive READY
20:04:49.179 primitive READY
20:04:49.188 primitive READY
20:04:49.191 primitive READY
20:04:49.196 primitive READY
20:04:49.198 primitive READY
main.a689a849a5162369e2e3.js:4329 20:04:49.198 addTransaction end
20:04:49.198 msg rcv end in 2.273
20:04:57.880 primitive promise set state=combining
20:04:57.880 primitive COMBINING
20:04:58.120 primitive CREATED
20:04:58.124 primitive CREATED
20:04:58.130 primitive CREATED
20:04:58.132 primitive CREATED
20:04:58.191 primitive COMBINED
20:04:58.192 msg rcv start
20:04:58.192 msg json parsed in 0.000
220:04:58.193 invokeSignalCallbacks end in 0.000
20:04:58.193 propertyUpdate end in 0.000
20:04:58.193 handlePropertyUpdate end in 0.001
20:04:58.193 msg rcv end in 0.001
20:04:58.260 primitive COMBINED
20:04:58.558 primitive COMPLETE
20:04:58.815 primitive COMPLETE
20:04:58.818 primitive promise set state=combining
20:04:58.818 primitive COMBINING
20:04:58.821 primitive promise set state=combining
20:04:58.821 primitive COMBINING
20:04:58.823 primitive promise set state=combining
20:04:58.824 primitive COMBINING
20:04:58.826 primitive promise set state=combining
20:04:58.826 primitive COMBINING
20:04:58.923 primitive COMBINED
20:04:58.927 primitive COMBINED
20:04:58.931 primitive COMBINED
20:04:58.994 primitive COMBINED
20:04:59.130 primitive COMBINED
20:04:59.305 primitive COMBINED
20:04:59.445 primitive COMBINED
20:05:00.110 primitive COMPLETE
20:05:00.435 primitive COMPLETE
20:05:00.678 primitive COMPLETE
20:05:00.811 primitive COMPLETE
20:05:01.153 primitive COMPLETE
20:05:01.499 primitive COMPLETE
20:05:01.638 primitive COMPLETE
20:05:01.653 primitive COMBINED
20:05:02.472 primitive COMPLETE
20:05:02.497 primitive COMBINED
20:05:02.808 primitive COMPLETE
20:05:03.890 primitive COMBINED
20:05:04.244 primitive COMPLETE
20:05:05.430 primitive COMBINED
20:05:05.836 primitive COMPLETE
20:05:06.692 primitive COMBINED
20:05:06.990 primitive COMPLETE
20:05:07.675 primitive COMBINED
20:05:07.835 primitive COMPLETE

Hi @Mickael,

For primitives, there is still a ready flag that is updated each frame. This is sufficient for CesiumJS internal renderer usage.

Your use case appears to be fairly specialized. May I ask what is the larger goal you are looking to accomplish?