WebGL never throws on a bad shader: compileShader() and linkProgram() fail silently until you ask with getShaderParameter(shader, gl.COMPILE_STATUS) or getProgramParameter(program, gl.LINK_STATUS), then read getShaderInfoLog() or getProgramInfoLog(). Compile messages read ERROR: 0:line: 'token' : text, so printing the offending source line beside each one saves counting. Link errors catch what no single shader can see, such as a vertex output and fragment input of different types:
function explain(source) {
try { GLH.shader(gl, gl.FRAGMENT_SHADER, source); } catch (e) {
const lines = source.split('\n');
for (const [, line, text] of e.message.matchAll(/ERROR: \d+:(\d+): (.*)/g)) {
console.log(`line ${line}: ${lines[line - 1].trim()}\n ${text}`);
}
}
}
explain(`#version 300 es
precision mediump float;
out vec4 outColor;
void main() {
outColor = vec4(0.5, 0.5, 0.5, 1);
outColor.a = 1;
}`);
try {
GLH.program(gl, `#version 300 es
out vec2 vUV; void main() { vUV = vec2(0.0); gl_Position = vec4(0.0); }`, `#version 300 es
precision mediump float; in vec3 vUV; out vec4 c; void main() { c = vec4(vUV, 1.0); }`);
} catch (e) { console.log(e.message); }line 6: outColor.a = 1; 'assign' : cannot convert from 'const int' to 'mediump float' Program failed to link: Types of varying 'vUV' differ between VERTEX and FRAGMENT shaders. FRAGMENT varying vUV does not match any VERTEX varying
Line 5 compiles because constructors convert; line 6 fails because assignment never does. Checking status makes the driver finish compiling, so with many programs compile and link them all first, then check, or poll COMPLETION_STATUS_KHR from the KHR_parallel_shader_compile extension, which this Chrome 1 exposes.
<!doctype html>
<style>
body { margin: 0; font: 12px system-ui, sans-serif; background: #f7f4ee; color: #333; }
.row { display: flex; gap: 10px; max-width: 600px; }
textarea { flex: 1; height: 170px; font: 11px/1.4 monospace; border: 1px solid #ccc; padding: 6px; }
canvas { width: 200px; height: 170px; flex: none; border: 1px solid #ccc; }
pre { max-width: 600px; margin: 6px 0 0; padding: 6px 8px; background: #fff; border: 1px solid #ddd;
font-size: 11px; white-space: pre-wrap; min-height: 60px; }
.bad { color: #b5452f; } .good { color: #3f7d3a; }
</style>
<div class="row">
<textarea id="src" spellcheck="false">#version 300 es
precision mediump float;
in vec2 vUV;
out vec4 outColor;
void main() {
outColor = vec4(vUV, 0.5, 1);
outColor.a = 1;
}</textarea>
<canvas id="view" width="200" height="170"></canvas>
</div>
<pre id="log"></pre>
<script>
const gl = document.getElementById('view').getContext('webgl2');
const logBox = document.getElementById('log');
const vsSource = `#version 300 es
in vec2 aPos; out vec2 vUV;
void main() { vUV = aPos * 0.5 + 0.5; gl_Position = vec4(aPos, 0.0, 1.0); }`;
// Shown whenever the edited shader fails: red warning stripes.
const errorShader = `#version 300 es
precision mediump float; out vec4 outColor;
void main() { float s = step(0.5, fract((gl_FragCoord.x + gl_FragCoord.y) / 24.0));
outColor = vec4(mix(vec3(0.71, 0.27, 0.18), vec3(0.97, 0.9, 0.85), s), 1.0); }`;
function compileWithLog(type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader); // never throws: we must ask
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) return { shader };
const lines = source.split('\n');
const notes = [...gl.getShaderInfoLog(shader).matchAll(/ERROR: \d+:(\d+): (.*)/g)]
.map(([, line, text]) => `line ${line}: ${(lines[line - 1] || '').trim()}\n ${text}`);
return { error: notes.join('\n') };
}
function link(vs, fs) {
const program = gl.createProgram();
gl.attachShader(program, vs); gl.attachShader(program, fs);
gl.bindAttribLocation(program, 0, 'aPos');
gl.linkProgram(program);
if (gl.getProgramParameter(program, gl.LINK_STATUS)) return { program };
return { error: 'Link error: ' + gl.getProgramInfoLog(program).trim() };
}
const vs = compileWithLog(gl.VERTEX_SHADER, vsSource).shader;
const fallback = link(vs, compileWithLog(gl.FRAGMENT_SHADER, errorShader).shader).program;
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, 1,1, -1,-1, 1,1, -1,1]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
function run() {
const fs = compileWithLog(gl.FRAGMENT_SHADER, document.getElementById('src').value);
const linked = fs.shader ? link(vs, fs.shader) : fs;
gl.useProgram(linked.program || fallback);
gl.drawArrays(gl.TRIANGLES, 0, 6);
logBox.className = linked.error ? 'bad' : 'good';
logBox.textContent = linked.error || 'Compiled and linked. (Try: in vec3 vUV; to see a link error.)';
}
document.getElementById('src').addEventListener('input', run);
run();
</script>