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!