#13717·mapbox-gl-js

drawFill reads layer.layout unguarded: layers added from a styledata listener are painted before recalculate

Author: guillem-aistechCreated Aug 20, 2026Updated Aug 20, 2026
Labelsbug :lady_beetle:auto-triaged

Summary

Style.update() fires styledata synchronously at its own tail, from inside Map._render(), after the per-layer recalculate() loop and before painter.render(). A layer added by a styledata listener therefore enters the render order for the current frame with layout === undefined, and is painted before it is ever evaluated.

drawFill reads layer.layout unguarded, so it throws. Two sibling functions in the same file already guard the identical access.

Version

Reproduced on v3.20.0. The unguarded read is still present on v3.28.1.

Error

Uncaught TypeError: Cannot read properties of undefined (reading 'get')
    at drawFill
    at Painter.renderLayer
    at Painter.render
    at Map._render
    at <requestAnimationFrame>

Mechanism

Within a single frame:

  1. Map._render()this.style.update(parameters)src/ui/map.ts:4537
  2. Style.update() recalculates layers — src/style/style.ts:1797
    typescript
    if (layer.visibility !== 'none' || layer.hasTransition()) layer.recalculate(parameters, this._availableImages);
  3. Style.update() then fires styledata at its tail — src/style/style.ts:1870
    typescript
    if (changed) {
        this.fire(new Event('data', {dataType: 'style'}));
    }
    A listener calling map.addLayer() here inserts a layer after step 2 has run.
  4. Map._render()this.painter.render(this.style, …)src/ui/map.ts:4570
  5. Painter.renderLayer guards only on isHidden() and coords.lengthsrc/render/painter.ts:1497-1499. It never checks layout.
  6. drawFillsrc/render/draw_fill.ts:80
    typescript
    if (layer.layout.get('fill-elevation-reference') !== 'none') {   // throws

layout is assigned only in StyleLayer.recalculate() (src/style/style_layer.ts:315-322), so the freshly added layer has none.

isHidden() does not save it either: when the addLayer spec has no layout block, visibility stays undefined, and isHidden() only returns true for the literal 'none' (src/style/style_layer.ts:301-305).

Minimal reproduction

javascript
map.on('load', () => {
    map.addSource('demo', {type: 'geojson', data: polygonsInViewport});
});

// Re-create the layer on each styledata. Note: no `layout` block in the spec.
map.on('styledata', () => {
    if (!map.isStyleLoaded()) return;
    if (map.getLayer('demo-fill')) map.removeLayer('demo-fill');
    map.addLayer({
        id: 'demo-fill',
        type: 'fill',
        source: 'demo',
        paint: {'fill-color': '#ff0000'}
    });
});

Then trigger any styledata while the polygons are inside the viewport.

The source must have renderable tiles on screen — otherwise renderLayer returns early on coords.length === 0 and never reaches drawFill. That is what makes this look intermittent.

Suggested fix

drawFill is the only unguarded consumer on this path. The same guard already exists twice in that file (v3.20.0 lines 151 and 235):

typescript
if (layer.layout && layer.layout.get('fill-elevation-reference') !== 'none') {
    elevationType = 'road';
}

Alternatively, skip layers with no layout in Painter.renderLayer, which would cover any other consumer reachable the same way.