createShader() returns an empty VERTEX_SHADER or FRAGMENT_SHADER object, shaderSource() stores the text and compileShader() compiles it; none of them reports failure. Ask with getShaderParameter(shader, pname), where pname is COMPILE_STATUS, SHADER_TYPE or DELETE_STATUS, and read the reasons with getShaderInfoLog(). The compiler continues past an error, so one log can list several:
const shader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(shader, `#version 300 es
precision mediump float; out vec4 outColor;
void main() {
vec3 c = vec3(1.0, 0.5);
outColor = vec4(c, 1.0) * uTint;
}`);
gl.compileShader(shader);
console.log('compiled?', gl.getShaderParameter(shader, gl.COMPILE_STATUS));
console.log(gl.getShaderInfoLog(shader).trim());Output
compiled? false ERROR: 0:4: 'constructor' : not enough data provided for construction ERROR: 0:5: 'uTint' : undeclared identifier
Each message reads ERROR: 0:<line>: '<token>' : <text>, counting lines from the start of your string. The wording is ANGLE 2,592 's and differs elsewhere, so rely on the line number, and test COMPILE_STATUS, not an empty log.
<!doctype html>
<style>
body { margin: 0; font: 12px system-ui, sans-serif; background: #f7f4ee; color: #333; }
.row { display: flex; gap: 10px; max-width: 600px; align-items: flex-start; }
canvas { width: 160px; height: 200px; flex: none; }
ol { flex: 1; margin: 0; padding: 6px 6px 6px 34px; background: #fff; border: 1px solid #ddd; font: 11px/1.5 monospace; }
li.bad { background: #f6dcd4; }
li .note { display: block; color: #b5452f; }
p { max-width: 600px; margin: 6px 0; }
</style>
<div class="row">
<ol id="source"></ol>
<canvas id="view" width="160" height="200"></canvas>
</div>
<p id="status"></p>
<script>
const gl = document.getElementById('view').getContext('webgl2');
const brokenSource = `#version 300 es
precision mediump float; out vec4 outColor;
void main() {
vec3 c = vec3(1.0, 0.5);
outColor = vec4(c, 1.0) * uTint;
}`;
// createShader, shaderSource and compileShader never report failure...
const shader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(shader, brokenSource);
gl.compileShader(shader);
// ...so ask: COMPILE_STATUS, SHADER_TYPE, and the info log.
const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
const type = gl.getShaderParameter(shader, gl.SHADER_TYPE) === gl.FRAGMENT_SHADER ? 'FRAGMENT_SHADER' : 'VERTEX_SHADER';
const log = gl.getShaderInfoLog(shader);
// Each message reads ERROR: 0:<line>: '<token>' : <text> -- attach it to its line.
const notes = {};
for (const [, line, text] of log.matchAll(/ERROR: \d+:(\d+): (.*)/g)) (notes[line] ||= []).push(text);
document.getElementById('source').innerHTML = brokenSource.split('\n').map((text, i) => {
const n = notes[i + 1];
const safe = text.replace(/&/g, '&').replace(/</g, '<');
return `<li class="${n ? 'bad' : ''}">${safe}${n ? n.map(m => `<span class="note">${m}</span>`).join('') : ''}</li>`;
}).join('');
document.getElementById('status').textContent =
`SHADER_TYPE: ${type} COMPILE_STATUS: ${compiled} messages: ${Object.values(notes).flat().length}. ` +
'Right: the fixed shader (vec3 with three values, uTint declared).';
// The corrected shader, drawn with the tint uniform it was missing.
const fixedSource = brokenSource.replace('vec3(1.0, 0.5)', 'vec3(1.0, 0.5, 0.2)')
.replace('out vec4 outColor;', 'out vec4 outColor; uniform vec4 uTint;');
const vs = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vs, `#version 300 es
void main() { gl_Position = vec4(gl_VertexID == 1 ? 3.0 : -1.0, gl_VertexID == 2 ? 3.0 : -1.0, 0.0, 1.0); }`);
gl.compileShader(vs);
const fs = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fs, fixedSource);
gl.compileShader(fs);
const program = gl.createProgram();
gl.attachShader(program, vs); gl.attachShader(program, fs);
gl.linkProgram(program);
gl.useProgram(program);
gl.uniform4f(gl.getUniformLocation(program, 'uTint'), 0.88, 0.8, 0.9, 1.0);
gl.bindVertexArray(gl.createVertexArray());
gl.drawArrays(gl.TRIANGLES, 0, 3);
</script>