ES6 in GTM Custom HTML: What text/gtmscript Really Does
By Eli C., MarTech & Analytics Engineer
Published Updated
Quick answer: Google Tag Manager compiles Custom HTML scripts as ES5, so
const, arrow functions or?.in a normal<script>block fail with a JavaScript compiler error. Writing<script type="text/gtmscript">gets the code past that step: itâs the type GTM itself gives Custom HTML scripts, so the runtime executes your block as written. Nothing transpiles or validates it, so use it only for syntax your visitorsâ browsers support, and test every change in Preview.
Why does GTM reject ES6 in Custom HTML tags?
GTM runs your Custom HTML through its own compiler before the code goes into the container. Googleâs Custom tags help page mentions it in passing: âTag Manager minifies the code for Custom HTML tags and this can change single quotes to double quotes.â That compiler parses JavaScript as ES5. Use ES2015 syntax and the workspace wonât build. Preview and Publish report a JavaScript compiler error that names the feature:
Error at line 6, character 27: This language feature is only supported for ECMASCRIPT_2015 mode or better: arrow function.
Custom JavaScript variables go through the same check. As of October 2026, the GTM release notes have no entry about ES6 support.
What does type=âtext/gtmscriptâ actually do?
It skips GTMâs compiler, not your browserâs parser. To see why that works, look at how a published container runs Custom HTML.
GTM stores each <script> from a Custom HTML tag in the container as <script type="text/gtmscript">. Browsers donât execute script elements with an unknown type, so when the tag fires, GTMâs runtime walks the inserted nodes and re-creates each one as a real script. This is the relevant branch of a live containerâs gtm.js, de-minified:
if (String(node.nodeName).toUpperCase() === 'SCRIPT' && node.type === 'text/gtmscript') {
var script = document.createElement('script')
script.async = false
script.type = 'text/javascript'
script.id = node.id
script.text = node.text // simplified: the runtime also falls back to other text properties
// ...then inserts it in place of the original node
}
When you write the type yourself, the compiler doesnât treat the block as JavaScript and passes it through. The runtime canât tell your block from one GTM prepared, so it runs it. Three things follow:
- No transpiling. An arrow function reaches the browser as an arrow function.
- No validation. A typo saves and publishes fine, then throws a
SyntaxErrorwhen the tag fires. - No minification. The block ships byte for byte.
You can confirm this on your own container. Preview a tag that uses the attribute, find the gtm.js request in the DevTools Network tab and search its response for a string from your code.
Google doesnât document the attribute. It works because of how the runtime is built today, and that can change in any release.
Does text/gtmscript work in every browser?
Only in browsers that support the syntax you use, because the code arrives untouched. A browser that canât parse it throws a SyntaxError and skips that one script; the rest of the container keeps running. Tag Assistant still reports the tag as fired, because for a Custom HTML tag that only means GTM inserted it.
| Syntax | Added in | Full support from |
|---|---|---|
const/let, arrow functions, template literals, destructuring | ES2015 | Chrome 49, Firefox 44, Safari 10 |
Optional chaining ?., nullish coalescing ?? | ES2020 | Chrome 80, Firefox 74, Safari 13.1 |
import/export statements | ES2015 modules | Not available: GTM creates a classic script, not a module |
If you still have to support browsers older than that, write ES5 or transpile outside GTM.
How do you test a text/gtmscript tag?
- Check the syntax before pasting. Save the script body to a file and run
node --check checkout.js. Nodeâs parser is newer than any browser you target, so this catches typos, not compatibility problems. - Preview with the console open. Trigger the tag and watch the browser console. A
SyntaxErrororReferenceErrorfrom your code shows up there, not in Tag Assistant. - Check the result, not the firing. If the tag pushes to the dataLayer, the event should appear in Tag Assistantâs event list with the values you expect.
- Test
{{Variables}}on purpose. In normal script blocks GTM compiles variable references into calls to its own runtime. I havenât found documentation on what it does with them inside a block it doesnât compile. Either confirm in Preview that the value resolves, or read the variable in an ordinary<script>block in the same tag, which GTM does compile. Scripts in one tag run in order:
<script>
window.__gtmCheckout = { currency: {{DLV - currency}} }
</script>
<script type="text/gtmscript">
const { currency } = window.__gtmCheckout ?? {}
</script>
Custom HTML tags have no built-in consent checks either. If the tag sends data anywhere, set Consent Settings â Require additional consent for tag to fire in its advanced settings. The Consent Mode post covers how those checks behave.
A working example: GA4 begin_checkout from a checkout widget
Say an embedded checkout widget dispatches a checkout:start DOM event with the cart in event.detail, and you canât change its code. This tag turns that into a GA4 begin_checkout push. Fire it on a trigger that runs before the widget does, such as Initialization - All Pages:
<script type="text/gtmscript">
window.addEventListener('checkout:start', ({ detail }) => {
const items = detail?.items ?? []
if (items.length === 0) return
const value = items.reduce((sum, item) => sum + item.price * (item.quantity ?? 1), 0)
window.dataLayer = window.dataLayer || []
window.dataLayer.push({ ecommerce: null }) // clear the previous ecommerce object
window.dataLayer.push({
event: 'begin_checkout',
ecommerce: {
currency: detail.currency,
value: Math.round(value * 100) / 100,
coupon: detail.coupon,
items: items.map(({ id, name, price, quantity = 1 }) => ({
item_id: id,
item_name: name,
price,
quantity
}))
}
})
})
</script>
Then send it with a GA4 Event tag named begin_checkout, on a Custom Event trigger for begin_checkout, with Send Ecommerce data set to the Data Layer. Two details matter:
- The event and parameter names follow Googleâs ecommerce guide, so GA4âs ecommerce reports pick them up without custom definitions.
- There is no transaction ID.
begin_checkoutdoesnât take one, and an ID generated in the browser fromDate.now()canât deduplicate apurchaseanyway. Use the order ID from your backend there.
When should you skip Custom HTML entirely?
- You own the site code. Push
begin_checkoutfrom the site itself. Thereâs no compiler in the way, and the event keeps working when the container changes. - The logic is reusable or loads a vendor script. Build a Custom Template. Templates run in sandboxed JavaScript with declared permissions, so a reviewer can see what the tag is allowed to touch.
- You need very old browsers. Write ES5, or transpile with Babel outside GTM and paste the output.
Cleaning up containers full of Custom HTML is a regular part of my tag management work.
Frequently asked questions
Does type="text/gtmscript" transpile ES6 to ES5?
No. It makes GTM's compiler skip the block, and GTM's runtime then runs the code exactly as you wrote it. A browser that can't parse the syntax throws a SyntaxError and skips that script.
Is text/gtmscript officially supported by Google?
No. It isn't in Google's Tag Manager documentation. It works because GTM uses the same type internally for Custom HTML scripts, and Google can change that without notice.
Do {{variables}} work inside a text/gtmscript block?
Test it in Preview before relying on it. I haven't found documentation on how GTM treats variable references in a block it doesn't compile. The safe option is to read the variable in an ordinary script block in the same tag and hand the value over.
Can I use ES6 in a GTM Custom JavaScript variable?
No. Custom JavaScript variables go through the same ES5 compiler, and the text/gtmscript attribute only applies to script elements inside Custom HTML tags. Write ES5 there.