Hi everyone,
After upgrading to Babylon.js 9.x and switching to the recommended dynamic loader registration (registerBuiltInLoaders() from @babylonjs/loaders/dynamic), loading any .glb/.gltf file fails in applications where window.Promise is replaced by another implementation. The most common case is Angular apps using zone.js, which replaces the global Promise with ZoneAwarePromise.
Error
Unable to load from https://assets.babylonjs.com/meshes/boombox.glb: toAsyncPlugin(...).importMeshAsync is not a function
Minimal repro (9.33.2, plain ES modules from CDN)
<!doctype html>
<html>
<body>
<canvas id="c" width="300" height="200"></canvas>
<!-- Remove this line and it works -->
<script src="https://cdn.jsdelivr.net/npm/zone.js@0.15.1/fesm2015/zone.js"></script>
<script type="importmap">
{ "imports": {
"@babylonjs/core/": "https://cdn.jsdelivr.net/npm/@babylonjs/core@9.33.2/",
"@babylonjs/loaders/": "https://cdn.jsdelivr.net/npm/@babylonjs/loaders@9.33.2/"
} }
</script>
<script type="module">
const { Engine } = await import('@babylonjs/core/Engines/engine.js');
const { Scene } = await import('@babylonjs/core/scene.js');
const { ArcRotateCamera } = await import('@babylonjs/core/Cameras/arcRotateCamera.js');
const { Vector3 } = await import('@babylonjs/core/Maths/math.vector.js');
const { ImportMeshAsync } = await import('@babylonjs/core/Loading/sceneLoader.js');
const { registerBuiltInLoaders } = await import('@babylonjs/loaders/dynamic.js');
registerBuiltInLoaders();
const engine = new Engine(document.getElementById('c'));
const scene = new Scene(engine);
new ArcRotateCamera('cam', 0, 1, 3, Vector3.Zero(), scene);
const res = await ImportMeshAsync('https://assets.babylonjs.com/meshes/boombox.glb', scene);
console.log('meshes', res.meshes.length);
</script>
</body>
</html>
- Without zone.js → loads fine (2 meshes).
- With zone.js → throws the error above.
Root cause
In packages/dev/core/src/Loading/sceneLoader.ts:
const createdPlugin = pluginFactory.createPlugin((pluginOptions ?? {}) as SceneLoaderPluginOptions);
plugin = createdPlugin instanceof Promise ? await createdPlugin : createdPlugin;
The createPlugin factories registered by registerBuiltInLoaders() are async functions, so they always return a native Promise. When window.Promise has been replaced (zone.js, some polyfills, or a Promise coming from another realm such as an iframe), nativePromise instanceof Promise evaluates to false. The Promise itself is then used as the plugin, and importMeshAsync is undefined on it.
Suggested fix
Keep the synchronous path for synchronous factories (as the code comment intends), but detect async results via duck typing instead of instanceof:
const createdPlugin = pluginFactory.createPlugin((pluginOptions ?? {}) as SceneLoaderPluginOptions);
plugin = typeof (createdPlugin as PromiseLike<unknown>)?.then === "function" ? await createdPlugin : createdPlugin;
I tested this exact change by patching @babylonjs/core@9.33.2/Loading/sceneLoader.js in the repro above. With zone.js loaded, the model loads correctly again.
It might also be worth checking for other instanceof Promise checks in the codebase that could hit the same issue.
Thanks a lot for the great work!