THREE.Object3D.add: object not an instance of THREE.Object3D.
Why three.js prints "THREE.Object3D.add: object not an instance of THREE.Object3D." when a loader result, a geometry or a material is passed to scene.add(), the check in Object3D.js that rejects it, and how the paprika3d plugin's open, look and eval replies show the missing object.
three.js logs this console error when scene.add() or group.add() receives something that is not a node of the scene graph. Nothing is thrown, so the page keeps running, but the thing you meant to add is not in the scene and never draws. In most reports the argument was the object a loader hands to its callback, which holds a scene but is not one.
Where three.js prints it
Object3D.add() in src/core/Object3D.js (three r186) accepts an argument when it has the isObject3D flag that every Object3D subclass sets. Anything else falls through to the error branch, and add() returns this as if it had worked:
if ( object && object.isObject3D ) {
object.removeFromParent();
object.parent = this;
this.children.push( object );
object.dispatchEvent( _addedEvent );
_childaddedEvent.child = object;
this.dispatchEvent( _childaddedEvent );
_childaddedEvent.child = null;
} else {
error( 'Object3D.add: object not an instance of THREE.Object3D.', object );
}
The error() helper from src/utils.js adds the THREE. prefix and passes the rejected value as a second argument, which is why Chrome prints it after the message ([object Object] for a plain object, [object BoxGeometry] for a geometry). That trailing word says what kind of value went in. The check reads a flag rather than using instanceof, so in r186 a mesh made by a second copy of three is still accepted here; that case prints a different warning, covered on the page for "Multiple instances of Three.js being imported".
Why scene.add rejects the argument
- The whole loader result instead of its scene.
GLTFLoadercalls back with an object holdingscene,scenes,animations,camerasandasset. Passing that object toscene.add()is the case in most threads; the node to add isgltf.scene.FBXLoaderandOBJLoaderhand over aGroupdirectly, so code copied from one of those into a glTF page breaks this way. - A geometry or material.
scene.add(new THREE.BoxGeometry())or adding a material by mistake. AMesh,Line,Points,Sprite,Group, light, camera or otherObject3Dcan go into the scene, and nothing else can; geometry and material go into aMeshfirst. - undefined or a Promise. Adding a variable before the async load has filled it, or adding the return value of
loadAsync()withoutawait, givesundefinedor aPromise. - An old copy of three that checked
instanceof. Pages that load a very old build next to a newer one, through a script tag and a bundler, can still hit the error with a real mesh. r186 checks the flag, so this is rare now.
A page that reproduces it
The page below builds a small crate, writes it to GLB bytes with GLTFExporter, and reads them back with GLTFLoader.parseAsync(), which resolves with the same object loader.load() passes to its callback. Then it adds that object itself:
const glb = await new GLTFExporter().parseAsync(box, { binary: true });
const gltf = await new GLTFLoader().parseAsync(glb, '');
scene.add(gltf); // the loader's result object, not gltf.scene
paprika3d is a Claude Code plugin and MCP server that opens a three.js page in its own headless Chrome on a paused clock. These calls ran on 2026-10-28 with the stable build, on a machine with no GPU; run folder paths are shortened to <project> and each reply is cut to the lines discussed here, never reworded.
open {"url": "index.html", "cwd": "<project>"}
Opened http://127.0.0.1:38273/index.html in 0.2 s, paused at frame 0.
Page text outside the canvas: none
{"three":"186","scenes":2,"entities":[]}
Errors so far:
THREE.Object3D.add: object not an instance of THREE.Object3D. [object Object]
observe {}
{"entities":{},"harness":{"frame":0,"renderer":{"calls":0,"triangles":0,"textures":0,"geometries":0,"programs":0},"scene":{"objects":1,"meshes":0,"lights":1,"cameras":0},"errors":["THREE.Object3D.add: object not an instance of THREE.Object3D. [object Object]"],"changed":null}}
look {}
On screen (0):
Never visible: none
The error fires while the page sets itself up, so open already lists it under Errors so far, with the [object Object] that says a plain object was passed. observe counts one object in the scene, the hemisphere light, and no meshes: the crate was never attached. look draws a frame and finds nothing on screen, which is how the bug looks to a player, a background colour and no model.
The same eval that runs code in the page shows the geometry variant of the message, with the rejected type named at the end:
eval {"code": "scene.add(new THREE.BoxGeometry(1, 1, 1)); return scene.children.length"}
2
errors: THREE.Object3D.add: object not an instance of THREE.Object3D. [object BoxGeometry]
What the replies do not show is which line of the page made the call: the message carries no stack in the reply, so the agent searches the page for .add( calls whose argument comes from a loader or a constructor that is not an Object3D.
The fix
Add gltf.scene. Keep the result object around if the page needs gltf.animations for an AnimationMixer or gltf.cameras later:
<!-- verify -->
<!doctype html>
<html><head><meta charset="utf-8"><title>Crate</title>
<style>html,body{margin:0;overflow:hidden}canvas{display:block}</style></head>
<body>
<script type="importmap">{"imports":{"three":"./node_modules/three/build/three.module.js","three/addons/":"./node_modules/three/examples/jsm/"}}</script>
<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1d2128);
scene.add(new THREE.HemisphereLight(0xffffff, 0x334455, 2));
const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.1, 100);
camera.position.set(2, 1.5, 3);
camera.lookAt(0, 0, 0);
// a GLB made in the page, standing in for a file loaded with loader.load('crate.glb')
const box = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshStandardMaterial({ color: 0xb07040 }));
box.name = 'crate';
const glb = await new GLTFExporter().parseAsync(box, { binary: true });
const gltf = await new GLTFLoader().parseAsync(glb, '');
scene.add(gltf.scene);
addEventListener('resize', () => {
renderer.setSize(innerWidth, innerHeight);
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
});
renderer.setAnimationLoop(() => renderer.render(scene, camera));
</script>
</body></html>
After the change, open has no Errors so far block, and the crate is drawn:
look {}
On screen (1):
crate 7.9% [474, 257, 794, 580]
Never visible: none
observe {}
{"entities":{"AuxScene":{"type":"Group","position":[0,0,0]}},"harness":{"frame":1,"renderer":{"calls":1,"triangles":12,"textures":1,"geometries":1,"programs":2},"scene":{"objects":3,"meshes":1,"lights":1,"cameras":0},"errors":[],"changed":null}}
AuxScene is the name GLTFExporter gives the scene it wraps around a single exported object; a file from Blender carries its own scene name. With the crate in place the renderer reports one draw call, and harness.errors is empty. For a loader callback written in the old style, the same rule applies: loader.load(url, gltf => scene.add(gltf.scene)).
Where people ask about it
These threads quote the message; each count was read on 2026-10-28.
- THREE.Object3D.add: object not an instance of THREE.Object3D (Stack Overflow, 37,703 views, 2 answers)
- "THREE.Object3D.add: object not an instance of THREE.Object3D". I get this error every time I try use a GLB model file with a GLTFLoader(). How can i fix this (discourse.threejs.org, 6,737 views, 2 posts)
- THREE.Object3D.add: object not an instance of THREE.Object3D (discourse.threejs.org, 2,589 views, 6 posts)
- three.min.js:2 THREE.Object3D.add: object not an instance of THREE.Object3D (Stack Overflow, 2,434 views, 1 answer)