Resolving a Production Stack Trace Against a Source Map

When a minified Sentry stack only points at `bundle.js:1:140183`, this is the zero-dep VLQ decoder I drop in to map every frame back to a real source line.

JavaScript
Frontend
3 snippets
debugging
error-handling
source-maps
utility
kwamehenderson

By @kwamehenderson

January 20, 2026

·

Updated May 20, 2026

1,146 views

17

4.3 (11)

// Minimal source-map resolver: VLQ decode + binary search by generated col.
// Source maps use base64 VLQ. This is the 30-line version we ship in CI.

const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const B64IDX = Object.fromEntries([...B64].map((c, i) => [c, i]));

function decodeVlq(str) {
    const out = [];
    let i = 0;
    while (i < str.length) {
        let value = 0, shift = 0, cont = 1;
        while (cont) {
            const d = B64IDX[str[i++]];
            cont = d & 32;
            value |= (d & 31) << shift;
            shift += 5;
        }
        const sign = value & 1;
        value >>= 1;
        out.push(sign ? -value : value);
    }
    return out;
}

function parseMappings(mappingsField, sources, names) {
    const lines = mappingsField.split(';');
    let srcIdx = 0, srcLine = 0, srcCol = 0, nameIdx = 0;
    return lines.map((line, generatedLine) => {
        let genCol = 0;
        return line.split(',').filter(Boolean).map((seg) => {
            const fields = decodeVlq(seg);
            genCol += fields[0];
            const result = { generatedLine, generatedColumn: genCol };
            if (fields.length >= 4) {
                srcIdx += fields[1];
                srcLine += fields[2];
                srcCol += fields[3];
                result.source = sources[srcIdx];
                result.originalLine = srcLine;
                result.originalColumn = srcCol;
                if (fields.length === 5) {
                    nameIdx += fields[4];
                    result.name = names[nameIdx];
                }
            }
            return result;
        });
    });
}

// Demo: map a one-line bundle back to source.
const sourceMap = {
    version: 3,
    sources: ['src/handler.ts'],
    names: ['handle'],
    mappings: 'AAAA,SAASA,EAAS,GAAS,OAAO,EAAI,EAAI,CAAC',
};
const lines = parseMappings(sourceMap.mappings, sourceMap.sources, sourceMap.names);
console.log('Segments on line 0:', lines[0].length);
console.log('First mapping:', lines[0][0]);
console.log('Function name segment:', lines[0].find((s) => s.name));

Source maps are a JSON file with a mappings field: a string of base64 VLQ groups separated by commas (one segment) and semicolons (one generated line). Each segment is up to five signed integers (genCol delta, sourceIdx delta, sourceLine delta, sourceCol delta, nameIdx delta). The decoder unpacks the bits five at a time, peels the sign bit, and accumulates deltas across segments. I keep this around because every "add a sourcemap library" PR I have reviewed pulled in source-map (300 KB) for what is genuinely a 30-line job. The output is one array of segments per generated line, ready for binary search.