A context is lost when the GPU resets (Windows restarts a driver that stalls for two seconds), the driver is updated, the GPU process crashes, a phone reclaims a background tab's memory, or a page opens too many contexts. Every GPU object is gone, and the canvas fires webglcontextlost. The WEBGL_lose_context extension triggers the same path on demand, so you can test your handler:
const gl = document.createElement('canvas').getContext('webgl2'), buffer = gl.createBuffer();
gl.canvas.addEventListener('webglcontextlost', (event) => {
event.preventDefault(); // "I will restore": required for contextrestored
console.log('webglcontextlost fired');
});
gl.getExtension('WEBGL_lose_context').loseContext(); // simulate a GPU reset
console.log(gl.isContextLost(), gl.getError(), gl.getError(),
gl.getParameter(gl.MAX_TEXTURE_SIZE), gl.isBuffer(buffer),
gl.createBuffer().constructor.name);
await new Promise((resolve) => setTimeout(resolve)); // the event comes in a later tasktrue 37442 0 null false WebGLBuffer webglcontextlost fired
Nothing throws: getError() reports CONTEXT_LOST_WEBGL (37442) once, queries return null, draws are ignored and createBuffer() returns a dead object. In the handler, which runs in a later task, cancel the animation loop and stop issuing GL calls. The loss you can cause by accident: Chrome 154 1 kept 16 contexts per page, and the 17th logged "Too many active WebGL contexts. Oldest context will be lost." and lost the first.
<!doctype html>
<style>
body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
.row { display: flex; gap: 10px; max-width: 600px; }
.cell { flex: 1; position: relative; }
canvas { display: block; width: 100%; background: #fff; border: 1px solid #ddd; box-sizing: border-box; }
.lost { position: absolute; inset: 0; display: none; align-items: center; justify-content: center;
color: #b5452f; font: bold 14px Georgia; background: repeating-linear-gradient(45deg, #fbeee9 0 10px, #fff 10px 20px); }
pre { max-width: 600px; margin: 6px 0 0; font: 11px monospace; }
</style>
<div class="row">
<div class="cell"><canvas id="a" width="290" height="200"></canvas></div>
<div class="cell"><canvas id="b" width="290" height="200"></canvas><div class="lost" id="lost">webglcontextlost</div></div>
</div>
<pre id="log"></pre>
<script>
const log = (text) => { document.getElementById('log').textContent += text + '\n'; };
function spinner(canvas) {
const gl = canvas.getContext('webgl2');
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
uniform float uAngle;
void main() { vec2 p = vec2(gl_VertexID & 1, gl_VertexID >> 1) * 2.0 - 1.0;
float c = cos(uAngle), s = sin(uAngle); p = mat2(c, s, -s, c) * (p * vec2(0.3, 0.45));
gl_Position = vec4(p.x * 0.69, p.y, 0.0, 1.0); }`], [gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float; out vec4 c; void main() { c = vec4(0.12, 0.37, 0.55, 1.0); }`]]) {
const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); gl.attachShader(program, s);
}
gl.linkProgram(program); gl.useProgram(program);
gl.bindVertexArray(gl.createVertexArray());
const state = { gl, request: 0, running: true };
const frame = (ms) => {
gl.clearColor(0.97, 0.96, 0.93, 1); gl.clear(gl.COLOR_BUFFER_BIT);
gl.uniform1f(gl.getUniformLocation(program, 'uAngle'), ms * 0.001);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
if (state.running) state.request = requestAnimationFrame(frame);
};
state.request = requestAnimationFrame(frame);
return state;
}
spinner(document.getElementById('a')); // keeps running
const b = spinner(document.getElementById('b')), gl = b.gl, buffer = gl.createBuffer();
gl.canvas.addEventListener('webglcontextlost', (event) => {
event.preventDefault(); // "I will restore": needed for contextrestored
b.running = false; cancelAnimationFrame(b.request); // stop issuing GL calls
document.getElementById('lost').style.display = 'flex';
log('webglcontextlost fired (in a later task)');
});
setTimeout(() => {
gl.getExtension('WEBGL_lose_context').loseContext(); // simulate a GPU reset
// Nothing throws: one CONTEXT_LOST_WEBGL error, null queries, ignored draws, dead objects.
log(`isContextLost ${gl.isContextLost()} | getError ${gl.getError()} then ${gl.getError()} (CONTEXT_LOST_WEBGL = 37442)`);
log(`MAX_TEXTURE_SIZE ${gl.getParameter(gl.MAX_TEXTURE_SIZE)} | isBuffer(old) ${gl.isBuffer(buffer)} | createBuffer() -> ${gl.createBuffer().constructor.name}`);
}, 600);
</script>