drawFill reads layer.layout unguarded: layers added from a styledata listener are painted before recalculate
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:4537Style.update()recalculates layers —src/style/style.ts:1797if (layer.visibility !== 'none' || layer.hasTransition()) layer.recalculate(parameters, this._availableImages);Style.update()then firesstyledataat its tail —src/style/style.ts:1870A listener callingif (changed) { this.fire(new Event('data', {dataType: 'style'})); }map.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:80if (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.
Source: mapbox/mapbox-gl-js