Upgrading to cesium 1.92: handleTileFailure -> error undefined - Tile is unloaded before the content finishes loading

Hi,

I’ve just updated to Cesium 1.92, where when.js has been replaced with native promises.

I’m now receiving a large stack trace, the main error shown below:

Unhandled Promise rejection: Cannot read properties of undefined (reading 'message') ; Zone: <root> ; Task: Promise.then ; Value: TypeError: Cannot read properties of undefined (reading 'message')
    at node_modules\@cesiumgs\cesium-analytics\Build\CesiumUnminified\Cesium.js:225729:1
    at ZoneDelegate.invoke (zone.js:372:1)
    at Zone.run (zone.js:134:1)
    at zone.js:1276:1
    at ZoneDelegate.invokeTask (zone.js:406:1)
    at Zone.runTask (zone.js:178:1)
    at drainMicroTaskQueue (zone.js:582:1)
    at ZoneTask.invokeTask [as invoke] (zone.js:491:1)
    at invokeTask (zone.js:1600:1)
    at XMLHttpRequest.globalZoneAwareCallback (zone.js:1637:1) TypeError: Cannot read properties of undefined 

This correlates to the below, from Cesium3DTileset.js:

function handleTileFailure(tileset, tile) {
  return function (error) {
    const url = tile._contentResource.url;
    const message = defined(error.message) ? error.message : error.toString();
    if (tileset.tileFailed.numberOfListeners > 0) {
      tileset.tileFailed.raiseEvent({
        url: url,
        message: message,
      });
    } else {
      console.log(`A 3D tile failed to load: ${url}`);
      console.log(`Error: ${message}`);
    }
  };
}

Where error is undefined.

This only happens when I navigate to a new route in an SPA, right after I zoom in on a tileset and you can see it is still loading.

Prior to navigating, the viewer is destroyed, and to my understanding primitives (and by extension tilesets) are also destroyed.

When interrogating the tileset from handleTileFailure, I can see that the tileset is not ready and several properties will return the error: “The tileset is not loaded. Use Cesium3DTileset.readyPromise or wait for Cesium3DTileset.ready to be true.”

From looking at the update method within Ceisum3DTileset.js, the requestTiles method is called if the tileset is ready, where requestContent is called which handles the errors for the tileset content ready promise handleTileFailure:

  tile.contentReadyPromise
    .then(handleTileSuccess(tileset, tile))
    .catch(handleTileFailure(tileset, tile));

contentReadyPromise is rejected within the following method:

function singleContentFailed(tile, tileset, error) {
  if (tile._contentState === Cesium3DTileContentState.PROCESSING) {
    --tileset.statistics.numberOfTilesProcessing;
  } else {
    --tileset.statistics.numberOfPendingRequests;
  }
  tile._contentState = Cesium3DTileContentState.FAILED;
  tile._contentReadyPromise.reject(error);
  tile._contentReadyToProcessPromise.reject(error);
}

At this point I can see:

tile.isDestroyed() == true
tile._contentState == Cesium3DTileContentState.FAILED;
error == undefined

singleContentFailed is called at line 1200 in Cesium3DTile.js, method requestSingleContent:

  promise
    .then(function (arrayBuffer) {
      if (tile.isDestroyed()) {
        // Tile is unloaded before the content finishes loading
        singleContentFailed(tile, tileset);
        return;
      }

singleContentFailed takes three arguments, (tile, tileset, error), and we can see error is missing here, causing the original error above where error is undefined.

So we can see that the promise is being reject due to: “Tile is unloaded before the content finishes loading”, where no error parameter is provided in the singleContentFailed.

Adding an error message removes most of the error messages, I now only receive:

Unhandled Promise rejection: Tile is unloaded before the content finishes loading ; 
Zone: <root> ; Task: Promise.then ; 
Value: Tile is unloaded before the content finishes loading undefined

...
Resource._Implementations.loadWithXhr	@	node_modules\@cesium…ied\Cesium.js:20200

I am using angular within zone.js, where zone.js wraps promises and catches any uncaught promises.

So it looks like somewhere there is a promise isn’t handling a rejection, looks like it has something to do with the deferred promise possibly.

Thanks,
Tom

1 Like

Alright, looks to be tile.contentReadyToProcessPromise isn’t being caught anywhere, adding a catch solves the issue.

I’ve just seen a PR for the missing error messages in singleContentFailed. I’ll comment on this PR to add catch onto the above promise is this is closely related.

PR: fix Cesium3DTile singleContentFailed have no error by jiangheng90 · Pull Request #10289 · CesiumGS/cesium · GitHub

1 Like

Hi @TomPovey,

Thanks for the update - I am glad that you were able to resolve the issue through a workaorund.

Also, thanks for bumping this.

-Sam

Hi,
I’ve updated cesium version from 1.66 to 1.104,
I followed this thread CesiumJS Ready Promise Deprecation / API Changes to update the way we are loading our tilesets but now i’m getting tons of errors regarding uncaught promise.

ERROR Error: Uncaught (in promise): [object Undefined]
    at resolvePromise (polyfills.js:881:23)
    at resolvePromise (polyfills.js:830:11)
    at polyfills.js:938:11
    at ZoneDelegate.invokeTask (polyfills.js:415:173)
    at Object.onInvokeTask (vendor.js:140477:25)
    at ZoneDelegate.invokeTask (polyfills.js:415:56)
    at Zone.runTask (polyfills.js:211:39)
    at drainMicroTaskQueue (polyfills.js:593:25)
    at ZoneTask.invokeTask (polyfills.js:496:13)
    at ZoneTask.invoke (polyfills.js:482:40)

Tilesets are loading but still i’m receiving a lot of errors, i’ve tried using try catch and .catch with .then but its not entering in it.

Also while moving to different locations and zooming in and out here and there, throws errors for same issue.

The tileset i’m using is huge and contains child tilesets in it. So, I thought it might be some issue with loading or unloading of inner tilesets.

I’ve just updated to 1.105 and am receiving the same, I haven’t had the time to look into this yet - did you manage to find the issue?
Thanks

I believe it is the same issue.

nope, I haven’t got anything yet.

Hi all,

Do you have a minimal code example that replicates the issue? That would help us understand the error. But ultimately what should be happening is that the error is caught and bubbled up to a non-critical error event.

Thanks,
Gabby

@Gabby_Getz
I’m also facing the same issue in the angular application, zone.js throwing the same error with 3D tilesets only.

@TomPovey
Do you have any solution for the given issue?

Hi @Gabby_Getz ,
here’s the code for loading the tileset,

try{
  var tileset= await Cesium.Cesium3DTileset.fromUrl(
    url,{
    maximumScreenSpaceError: 256,
    maximumMemoryUsage: 4096,
    skipLevelOfDetail: true,
    preferLeaves: true,
  });
  this.viewer.scene.primitives.add(tileset);
}catch(e){
  console.log(e)
}

Thanks @Saurav_Kumar,

It would help to have the tileset data as well. Do you have a URL? If you need to keep data private, you can send it in an email to support@cesium.com.

Seeing the same thing with Angular 13 and Cesium 1.105. I’m digging through to see what is not being handled and making Zone.js unhappy.

Results from digging in and a patch that seems to fix Zone.js throwing that error:
This seems to originate from Resource.prototype._makeRequest lines 1394 and 1406 inside the request’s catch handler. It appears that somehow the parameter into the catch can be undefined (most common when the request was RequestState.CANCELLED). Protecting the calls into the Promise.reject(e) seemed to help.

Diff:

diff --git a/packages/engine/Source/Core/Resource.js b/packages/engine/Source/Core/Resource.js
index cf4c0dfce0..6cab75abe1 100644
--- a/packages/engine/Source/Core/Resource.js
+++ b/packages/engine/Source/Core/Resource.js
@@ -1391,7 +1391,7 @@ Resource.prototype._makeRequest = function (options) {
     .catch(function (e) {
       request.cancelFunction = undefined;
       if (request.state !== RequestState.FAILED) {
-        return Promise.reject(e);
+        return defined(e) && Promise.reject(e);
       }

       return resource.retryOnError(e).then(function (retry) {
@@ -1403,7 +1403,7 @@ Resource.prototype._makeRequest = function (options) {
           return resource.fetch(options);
         }

-        return Promise.reject(e);
+        return defined(e) && Promise.reject(e);
       });
     });
 };

Yes this is what’s causing zone.js to complain, Promise.reject() isn’t being handled.

Within Cesium3DTile.js, line 1294:

const promise = resource.fetchArrayBuffer();

calls the makeRequest snippet you posted. Adding a simple .catch() at the end of this catch chain causes the rejected promise to be handled edit don’t do this, see my below edit:

  return promise
    .then(function (data) {
      // explicitly set to undefined to ensure GC of request response data. See #8843
      request.cancelFunction = undefined;
      return data;
    })
    .catch(function (e) {
      request.cancelFunction = undefined;
      if (request.state !== RequestState.FAILED) {
        return Promise.reject(e);
      }

      return resource.retryOnError(e).then(function (retry) {
        if (retry) {
          // Reset request so it can try again
          request.state = RequestState.UNISSUED;
          request.deferred = undefined;

          return resource.fetch(options);
        }

        return Promise.reject(e);
      });
    }).catch(); // <------------

Or just: Promise.reject(e).catch().

I’ll dig a little deeper

EDIT don’t do the above or return undefined from the makeRequest method otherwise the below snippet won’t be called:

Ok so the issue is the processArrayBuffer function within Cesium3DTile.js.

In here, the passed in promise is within a try/catch block:

async function processArrayBuffer(
  tile,
  tileset,
  request,
  expired,
  requestPromise
) {
  const previousState = tile._contentState;
  tile._contentState = Cesium3DTileContentState.LOADING;
  ++tileset.statistics.numberOfPendingRequests;

  let arrayBuffer;
  try {
    arrayBuffer = await requestPromise;
  } catch (error) {
    --tileset.statistics.numberOfPendingRequests;
    if (tile.isDestroyed()) {
      // Tile is unloaded before the content can process
      return;
    }

    if (request.cancelled || request.state === RequestState.CANCELLED) {
      // Cancelled due to low priority - try again later.
      tile._contentState = previousState;
      ++tileset.statistics.numberOfAttemptedRequests;
      return;
    }

    tile._contentState = Cesium3DTileContentState.FAILED;
    throw error;
  }
...

Although I’m unsure why this is causing an error, since we are still catching the error in the try/catch block which is permitted for async operations. Must be something to do with hoe zone.js wraps native promises. Updating it to this works:

async function processArrayBuffer(
  tile,
  tileset,
  request,
  expired,
  requestPromise
) {
  const previousState = tile._contentState;
  tile._contentState = Cesium3DTileContentState.LOADING;
  ++tileset.statistics.numberOfPendingRequests;

  let arrayBuffer = await requestPromise.catch(error => {
    --tileset.statistics.numberOfPendingRequests;
    if (tile.isDestroyed()) {
      // Tile is unloaded before the content can process
      return;
    }

    if (request.cancelled || request.state === RequestState.CANCELLED) {
      // Cancelled due to low priority - try again later.
      tile._contentState = previousState;
      ++tileset.statistics.numberOfAttemptedRequests;
      return;
    }

    tile._contentState = Cesium3DTileContentState.FAILED;
    throw error;
  });
...

The throw error is still unhandled, but I haven’t come across this - we’ll have to have a look at the calleres further up the chain to see where this needs to be handled

1 Like

Hello,

This is still happening on version 1.117.0.
Applying this fix seems to work well

Unfortunately the error remains.

Edit:
It seems that the patch, after an npm i, doesn’t apply the changes on index.cjs file. Still trying to figure it out how to do it.

Edit2:
Was able to do a workaround but well, it’s a workaround, this should be reviewed.

Thanks!

Hello,

Same error here with 1.118.2.

const url = ...;
Cesium.Cesium3DTileset.fromUrl(url)
.then((tileset:any) => {
     this.tilesetPrimitiveCollection.add(tileset);
}).catch((err:any) => console.log(url, err));

Could you describe your workaround please ?

Regards

Hello,

I’ve build cesium from its repo and then I copy those files to node_modules:

With (after every npm i):
image

Not sure why a patch was not working as expected, so I went with this solution.

Good luck!

Hi all,

Since this is affecting so many users, I opened Bad error handling causing issues in Angular · Issue #12031 · CesiumGS/cesium · GitHub to track the issue. We’d be happy to accept a PR to fix!