Linking Programs

Linking a Program and Reading Link Errors

createProgram(), two attachShader() calls and linkProgram() join the stages into one executable. Linking checks what no single shader can: that both stages are present and compiled, that every fragment in has a matching vertex out, and that a uniform declared in both stages agrees in type and precision:

Link failures as ANGLE reports themJavaScript
const compile = (type, src) => {
  const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); return s;
};
const vs = compile(gl.VERTEX_SHADER, `#version 300 es
  uniform highp float uScale; void main() { gl_Position = vec4(uScale); }`);
const fs = (body) => compile(gl.FRAGMENT_SHADER,
  `#version 300 es\nprecision mediump float; out vec4 o; ${body}`);
for (const [label, ...shaders] of [['vertex only', vs],
  ['missing varying', vs, fs('in vec3 vNormal; void main() { o = vec4(vNormal, 1.0); }')],
  ['uniform precision', vs, fs('uniform float uScale; void main() { o = vec4(uScale); }')]]) {
  const program = gl.createProgram();
  for (const s of shaders) gl.attachShader(program, s);
  gl.linkProgram(program);
  console.log(`${label}:`, gl.getProgramParameter(program, gl.LINK_STATUS) ||
    gl.getProgramInfoLog(program).trim());
}
Output
vertex only: The program must contain objects to form both a vertex and fragment shader.
missing varying: FRAGMENT varying vNormal does not match any VERTEX varying
uniform precision: Precisions of uniform 'uScale' differ between VERTEX and FRAGMENT shaders.

A linked program keeps its code, so you may then detachShader() and deleteShader() its shaders. validateProgram() checks a program against the current state: a sampler2D and a samplerCube both left on texture unit 0 fail with "Two textures of different types use the same sampler location." Use it only while debugging.

Four programs linked side by side: three link failures with ANGLE's log and one successHTMLLive
<!doctype html>
<style>
  body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
  canvas { display: block; width: 100%; max-width: 600px; }
  .names { display: flex; max-width: 600px; }
  .names div { flex: 1; padding: 4px; text-align: center; }
  .names b { display: block; font-size: 12px; }
  .bad { color: #b5452f; } .good { color: #3f7d3a; }
</style>
<canvas id="view" width="600" height="150"></canvas>
<div class="names" id="names"></div>
<script>
const gl = document.getElementById('view').getContext('webgl2');
const compile = (type, src) => {
  const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); return s;
};
// Every vertex shader here draws a full-viewport triangle.
const vsBody = `void main() {
  gl_Position = vec4(gl_VertexID == 1 ? 3.0 : -1.0, gl_VertexID == 2 ? 3.0 : -1.0, 0.0, 1.0) * uScale;
  gl_Position.w = 1.0; vUV = gl_Position.xy * 0.5 + 0.5; }`;
const vs = compile(gl.VERTEX_SHADER, `#version 300 es
uniform highp float uScale; out vec2 vUV; ${vsBody}`);
const fs = (body) => compile(gl.FRAGMENT_SHADER,
  `#version 300 es\nprecision mediump float; out vec4 o; ${body}`);

const cases = [
  ['vertex only', vs],
  ['missing varying', vs, fs('in vec3 vNormal; void main() { o = vec4(vNormal, 1.0); }')],
  ['uniform precision', vs, fs('uniform float uScale; void main() { o = vec4(uScale); }')],
  ['matching stages', vs, fs('in vec2 vUV; void main() { o = vec4(vec3(0.12, 0.37, 0.55) * (0.6 + 0.6 * vUV.y), 1.0); }')],
];
gl.bindVertexArray(gl.createVertexArray());
gl.enable(gl.SCISSOR_TEST);
let html = '';
cases.forEach(([label, ...shaders], i) => {
  const program = gl.createProgram();
  for (const s of shaders) gl.attachShader(program, s);
  gl.linkProgram(program);
  const ok = gl.getProgramParameter(program, gl.LINK_STATUS);
  gl.viewport(i * 150 + 10, 10, 130, 130);
  gl.scissor(i * 150 + 10, 10, 130, 130);
  if (ok) {
    gl.useProgram(program);
    gl.uniform1f(gl.getUniformLocation(program, 'uScale'), 1.0);
    gl.drawArrays(gl.TRIANGLES, 0, 3);
  } else {                                     // nothing to draw with: a pale red tile
    gl.clearColor(0.93, 0.8, 0.76, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);
  }
  const log = ok ? 'LINK_STATUS true' : gl.getProgramInfoLog(program).trim().split('\n')[0];
  html += `<div><b>${label}</b><span class="${ok ? 'good' : 'bad'}">${log.replace(/</g, '&lt;')}</span></div>`;
});
document.getElementById('names').innerHTML = html;
</script>