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:
Map._render() → this.style.update(parameters) — src/ui/map.ts:4537
Style.update() recalculates layers — src/style/style.ts:1797
if (layer.visibility !== 'none' || layer.hasTransition()) layer.recalculate(parameters, this._availableImages);
Style.update() then fires styledata at its tail — src/style/style.ts:1870
if (changed) {
this.fire(new Event('data', {dataType: 'style'}));
}
A listener calling map.addLayer() here inserts a layer after step 2 has run.
Map._render() → this.painter.render(this.style, …) — src/ui/map.ts:4570
Painter.renderLayer guards only on isHidden() and coords.length — src/render/painter.ts:1497-1499. It never checks layout.
drawFill — src/render/draw_fill.ts:80
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
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):
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.
Summary
Style.update()firesstyledatasynchronously at its own tail, from insideMap._render(), after the per-layerrecalculate()loop and beforepainter.render(). A layer added by astyledatalistener therefore enters the render order for the current frame withlayout === undefined, and is painted before it is ever evaluated.drawFillreadslayer.layoutunguarded, 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
Mechanism
Within a single frame:
Map._render()→this.style.update(parameters)—src/ui/map.ts:4537Style.update()recalculates layers —src/style/style.ts:1797Style.update()then firesstyledataat its tail —src/style/style.ts:1870map.addLayer()here inserts a layer after step 2 has run.Map._render()→this.painter.render(this.style, …)—src/ui/map.ts:4570Painter.renderLayerguards only onisHidden()andcoords.length—src/render/painter.ts:1497-1499. It never checkslayout.drawFill—src/render/draw_fill.ts:80layoutis assigned only inStyleLayer.recalculate()(src/style/style_layer.ts:315-322), so the freshly added layer has none.isHidden()does not save it either: when theaddLayerspec has nolayoutblock,visibilitystaysundefined, andisHidden()only returnstruefor the literal'none'(src/style/style_layer.ts:301-305).Minimal reproduction
Then trigger any
styledatawhile the polygons are inside the viewport.The source must have renderable tiles on screen — otherwise
renderLayerreturns early oncoords.length === 0and never reachesdrawFill. That is what makes this look intermittent.Suggested fix
drawFillis the only unguarded consumer on this path. The same guard already exists twice in that file (v3.20.0 lines 151 and 235):Alternatively, skip layers with no
layoutinPainter.renderLayer, which would cover any other consumer reachable the same way.