THREE.WebGLProgram: Shader Error 0 - VALIDATE_STATUS false
What three.js means by "THREE.WebGLProgram: Shader Error 0 - VALIDATE_STATUS false", why the 0 and the false say nothing about the bug, how to map the GLSL line number back to your shader, and how a coding agent gets the whole compiler log from the paprika3d plugin's look and observe replies.
three.js logs this when the GPU driver rejects a material's shader. The first line is the same for almost every broken shader, so it tells you very little; the useful part is further down the same console message: the material's name and type, the driver's compile log with a line number, and the shader source around that line. Any mesh drawn with that material is skipped until the shader compiles.
Where three.js prints it
src/renderers/webgl/WebGLProgram.js (three r186) compiles and links the program, then checks the result the first time the renderer uses it (onFirstUse, when renderer.debug.checkShaderErrors is true, which is the default). When the link status is false and no renderer.debug.onShaderError callback is set, it builds one long message:
const vertexErrors = getShaderErrors( gl, glVertexShader, 'vertex' );
const fragmentErrors = getShaderErrors( gl, glFragmentShader, 'fragment' );
error(
'WebGLProgram: Shader Error ' + gl.getError() + ' - ' +
'VALIDATE_STATUS ' + gl.getProgramParameter( program, gl.VALIDATE_STATUS ) + '\n\n' +
'Material Name: ' + self.name + '\n' +
'Material Type: ' + self.type + '\n\n' +
'Program Info Log: ' + programLog + '\n' +
vertexErrors + '\n' +
fragmentErrors
);
The two values on the first line are not about your shader. gl.getError() is the WebGL error flag, and a failed compile does not set it, so it is nearly always 0 (no error). three never calls gl.validateProgram(), so VALIDATE_STATUS stays false for every program, working or not. People who search for "shader error 0" are looking at the two least useful words of the message.
getShaderErrors reads each shader's info log; when the log names a line (ERROR: 0:60), it adds a slice of the source around it through handleSource, marking the line with >.
Why a shader fails to compile
- GLSL types do not convert by themselves.
float x = 1;or1 - someFloatis an error in GLSL ES: an integer literal is anint, and nothing turns it into afloat. This is the most common hand-written mistake. - A name three does not declare. A
ShaderMaterialgetsposition,uv,normaland the camera matrices for free; aRawShaderMaterialgets nothing, and a custom attribute needs its ownattributeline. - A chunk edited through
onBeforeCompilethat replaces an#includewith code using a variable that a newer three release renamed. Updating three breaks the patch, not the material. - Driver limits and precision. A shader that compiles on a desktop can fail on a phone: too many uniforms or samplers, or a constant out of range for
lowp. - A loaded model's material with an extension or a combination of maps that a specific three release or driver handles wrongly, as in the r151 thread above; the log then points into three's own chunks, and updating three or the driver is the fix.
A page that reproduces it
A ShaderMaterial named glow subtracts from an integer literal:
fragmentShader: `uniform vec3 uColor;
varying vec2 vUv;
void main() {
float glow = 1 - distance(vUv, vec2(0.5)) * 2.0;
gl_FragColor = vec4(uColor * glow, 1.0);
}`
paprika3d is a Claude Code plugin and MCP server; it loads a three.js page in a headless Chrome of its own and holds the clock at frame 0. The replies here were captured on 2026-10-28 from the stable build on a host without a GPU. They are trimmed to the lines discussed, never edited, and show the project folder as <project>.
open {"url":"index.html","cwd":"<project>"}
Opened http://127.0.0.1:41441/index.html in 0.2 s, paused at frame 0.
Page text outside the canvas: none
{"three":"186","scenes":1,"entities":["disc"]}
look {}
Pixels: brightness 0%, colour saturation 0%, 100% near black, 0% blown out. Mostly black: a blank or unlit frame? Pale: little colour.
On screen (1):
disc 32% [354, 114, 925, 685] 2 × 2 units approximate (a sprite or its own shader: see the picture)
Never visible: none
Page errors since the last call (1):
THREE.WebGLProgram: Shader Error 0 - VALIDATE_STATUS false
A shader failed to compile: what is drawn with it draws nothing (a look lists it never visible) until it compiles.
observe {}
{"entities":{"disc":{"type":"Mesh","position":[0,0,0]}},"harness":{"frame":1,"renderer":{"calls":1,"triangles":2,"textures":0,"geometries":1,"programs":2},"scene":{"objects":1,"meshes":1,"lights":0,"cameras":0},"errors":["THREE.WebGLProgram: Shader Error 0 - VALIDATE_STATUS false\n\nMaterial Name: glow\nMaterial Type: ShaderMaterial\n\nProgram Info Log: Fragment shader is not compiled.\n\nFRAGMENT\n\nERROR: 0:60: '-' : wrong operand types - no operation '-' exists that takes a left-hand operand of type 'const int' and a right operand of type 'highp float' (or there is no acceptable conversion)\nERROR: 0:60: '=' : cannot convert from 'const int' to 'highp float'\n\n 55: }\n 56: \n 57: uniform vec3 uColor;\n 58: varying vec2 vUv;\n 59: void main() {\n> 60: float glow = 1 - distance(vUv, vec2(0.5)) * 2.0;\n 61: gl_FragColor = vec4(uColor * glow, 1.0);\n 62: }"],"changed":null}}
open is clean because three compiles a program at the first render, and open stops before it. look draws that frame: its Page errors block shows the first line of the message and adds a line saying a shader failed. The picture is all black (100% near black). The disc row in the list comes from the mesh's position and size, so it still reads 32% of the frame even though no pixel of it was drawn; trust the Pixels line here.
observe returns the whole console message under harness.errors. The compiler says '=' : cannot convert from 'const int' to 'highp float' at line 60, and the source slice marks that line with >. The number counts the lines three puts in front of your code (version, precision, defines and uniforms), which is why a six-line shader errors at line 60: the slice shows your own text starting at line 57, so line 60 is the fourth line of fragmentShader.
The fix
Write the literal as a float, 1.0. The fixed page also clamps the value so the corners stay black instead of going negative:
<!-- verify -->
<!doctype html>
<html><head><meta charset="utf-8"><title>Glow</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';
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);
const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.1, 100);
camera.position.set(0, 0, 3);
const material = new THREE.ShaderMaterial({
name: 'glow',
uniforms: { uColor: { value: new THREE.Color(0x33ccff) } },
vertexShader: `varying vec2 vUv;
void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
fragmentShader: `uniform vec3 uColor;
varying vec2 vUv;
void main() {
float glow = 1.0 - distance(vUv, vec2(0.5)) * 2.0; // 1.0, not 1: GLSL never turns an int into a float
gl_FragColor = vec4(uColor * max(glow, 0.0), 1.0);
}`
});
const disc = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);
disc.name = 'disc';
scene.add(disc);
addEventListener('resize', () => {
renderer.setSize(innerWidth, innerHeight);
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
});
renderer.setAnimationLoop(() => renderer.render(scene, camera));
</script>
</body></html>
After the fix the frame has colour and no error is listed:
look {}
Pixels: brightness 13%, colour saturation 43%, 10% near black, 0% blown out.
On screen (1):
disc 32% [354, 114, 925, 685] 2 × 2 units approximate (a sprite or its own shader: see the picture)
Never visible: none
observe {}
{"entities":{"disc":{"type":"Mesh","position":[0,0,0]}},"harness":{"frame":1,"renderer":{"calls":1,"triangles":2,"textures":0,"geometries":1,"programs":2},"scene":{"objects":1,"meshes":1,"lights":0,"cameras":0},"errors":[],"changed":null}}
To catch the next shader mistake without taking a picture, an agent can build every program in the scene from eval. On the broken page, right after open, that reply carries the message on its errors line, along with Chrome's own warnings about the invalid program:
eval {"code":"renderer.compile(scene, camera); return renderer.info.programs.length"}
1
warn: WebGL: INVALID_OPERATION: useProgram: program not valid
warn: WebGL: INVALID_OPERATION: drawElements: no valid shader program in use
errors: THREE.WebGLProgram: Shader Error 0 - VALIDATE_STATUS false
For your own logging, set renderer.debug.onShaderError to a function; three then calls it instead of printing this message.
Where people ask about it
These threads quote the message; each count was read on 2026-10-28.
- Webgl shader error 0 (discourse.threejs.org, 5,038 views, 14 posts)
- Shader error ("must have compiled vertex shader attached") (discourse.threejs.org, 3,627 views, 2 posts)
- Shader error Overflow in implicit constant conversion, minimum range for lowp float is (-2,2) (discourse.threejs.org, 3,102 views, 10 posts)
- Shader Error 0 - VALIDATE_STATUS false (discourse.threejs.org, 2,060 views, 3 posts)
- WebGLProgram Shader Error when loading glb model in three.js r151 (GitHub, 6 comments)