A tiny JavaScript debug helper: put trace(); at the top of a function and it logs
Called: <functionName> — working the name out for itself. Silent by default, so it can't
ship noise to production.
function loadUser(id) {
trace(); // → Called: loadUser
...
}Zero dependencies. One file. ~30 lines. MIT.
Most of us have typed some version of this:
console.log('Called:', arguments.callee.name); // don'tTwo problems. arguments.callee is deprecated and throws in strict mode, ES modules and
class bodies — which is most modern code. And those console.logs have a habit of ending up in
production.
trace() fixes both: it reads the name off the stack instead of arguments.callee, and it does
nothing at all unless you explicitly turn it on.
Drop the file in — the primary way to use this. Copy trace.js into your project:
<script src="/js/trace.js"></script>
<script>
trace.enabled = true; // see "Enabling it" below for the grown-up version
</script>It defines a global trace. Load it before the code you want to trace.
Or load it from a CDN (jsDelivr, served straight from this repo's tags — no install):
<!-- Pinned to a version (recommended) -->
<script src="https://cdn.jsdelivr.net/gh/1978io/trace@1.0.0/trace.js"></script>
<!-- Or always the latest -->
<script src="https://cdn.jsdelivr.net/gh/1978io/trace/trace.js"></script>Pin to a tag (@1.0.0) for a stable, immutable URL; the untagged form tracks the default branch.
Or copy the source. It's one small file with no dependencies — pasting it into your existing utilities bundle is a perfectly good answer, and the licence allows it.
Or npm (see package.json — publishing is a follow-up, the file drop is the
supported route today):
const trace = require('@1978io/trace'); // CommonJS
import trace from '@1978io/trace'; // Node ESM (via the CommonJS default export)function saveDraft() {
trace();
}
saveDraft();
// Called: saveDraftfunction loadUser(id) {
trace(id);
}
loadUser(7);
// Called: loadUser 7Arguments are passed straight through to console.log, so objects, arrays and DOM nodes stay
inspectable in devtools rather than being stringified.
This is the one worth remembering. Wrap your variables in an object literal and ES6 shorthand keeps the variable names alongside the values:
function applyDiscount(code) {
const total = 42.5;
const tier = 'gold';
trace({ code, total, tier });
}
applyDiscount('SPRING');
// Called: applyDiscount { code: 'SPRING', total: 42.5, tier: 'gold' }Compare trace(code, total, tier) → Called: applyDiscount SPRING 42.5 gold, where you have to
remember which value is which. Three extra characters buys you self-labelling output.
trace() is off by default. Dropping the file into a project does nothing until you say
otherwise — no surprise console spam, no "who added these logs".
trace.on(); // enable
trace.off(); // disable
trace.enabled = true; // same thing, if you prefer the flagThe flag is read live, on every call. So on a deployed page you can open devtools and type:
trace.enabled = true…and the very next call traces. No reload, no rebuild, no special build of the app.
Do it explicitly, from your environment — the same source of truth that already tells your app whether it's in development.
Server-rendered (PHP, and the same idea in any templating language):
<script src="/js/trace.js"></script>
<script>
trace.enabled = <?= APP_ENV === 'development' ? 'true' : 'false' ?>;
</script>Static / bundled — set it from a build flag or a config value you control:
import trace from '@1978io/trace';
trace.enabled = process.env.NODE_ENV !== 'production';// or from your own config object, whatever shape it takes
trace.enabled = APP_CONFIG.debug === true;The tempting one-liner:
trace.enabled = location.hostname === 'localhost'; // ✗ don'tIt looks equivalent. It isn't — and it fails silently, which is the worst way to fail.
The moment anyone works on the project at a custom local vhost (trace.test, myapp.local), via
a container or VM hostname, over an IP on the office LAN, from a phone on 192.168.1.x, in a
preview deploy, or on staging, the check quietly returns false. Tracing just… doesn't happen.
There's no error and nothing in the console to explain it, so the developer concludes the library
is broken, or worse, that the function they're tracing is never called. That's an afternoon lost
to a one-line assumption.
Variants like hostname.includes('local') or a regex over .test/.dev domains are the same
bug with more characters — they encode a guess about everyone's machine rather than a fact about
the environment.
Your environment already knows whether it's development. Ask it.
If trace.enabled is false, the very first line of trace() returns. Nothing is logged, no
Error is constructed, no stack is parsed — the cost is one property read.
That's the guarantee: a trace() left in shipped code is inert. It doesn't leak variable values
into a user's console, doesn't show up in error-reporting noise, and doesn't need stripping at
build time. If you'd rather it wasn't there at all, it's still just a line to delete — but nothing
breaks if you forget.
There's no supported API for "what function am I in". trace() constructs an Error, reads its
.stack, and takes the frame above itself:
Error ← V8 header line, filtered out
at trace (trace.js:14:23) ← [0] trace itself
at loadUser (app.js:31:3) ← [1] the caller — this is the one we want
at handleSubmit (app.js:88:5)
A regex pulls the name out. Both major stack formats are handled:
| Engine | Frame format | Parsed as |
|---|---|---|
| V8 — Chrome, Edge, Node, Deno | at loadUser (app.js:31:3) |
loadUser |
| SpiderMonkey / JavaScriptCore — Firefox, Safari | loadUser@app.js:31:3 |
loadUser |
Method and constructor frames come through with their context — you'll see Widget.render,
Object.method, and Foo for new Foo().
It's best-effort, and honest about it. When there's no name to find — an anonymous callback,
an IIFE, a frame that's just a URL — you get (anon):
[1].forEach(function () {
trace('nothing to name here');
});
// Called: (anon) nothing to name hereTwo things worth knowing:
- Minifiers rename functions. In a minified production bundle you'd see
Called: n. Since tracing is a development tool that's off in production, this rarely bites — but don't build anything on the string. - Modern engines infer more names than you'd expect.
const mystery = function () {}reports asmystery, because the assignment gives the function expression a name. Genuinely anonymous positions — callbacks passed inline, IIFEs — are the ones that fall back to(anon).
It never throws. The stack parsing and the log call are both wrapped: a missing .stack, an
exotic engine format, or a custom sink that blows up all result in trace() returning quietly.
A debugging tool that breaks the app it's debugging is worse than no tool at all.
Route output somewhere other than console.log:
trace.sink = function (label, name, ...rest) {
logPanel.append(`${label} ${name} ${rest.join(' ')}`);
};The sink receives exactly what console.log would have: 'Called:', the resolved name, then your
arguments. Set it back to null to return to the console. demo.html uses a sink to
mirror output onto the page while still logging normally.
Useful for piping traces into an on-screen panel while debugging on a phone, or into a test harness that asserts a function was reached.
trace(...args) |
Log Called: <name> plus any arguments. No-op when disabled. |
trace.enabled |
boolean, default false. Read live on every call. |
trace.on() / trace.off() |
Set the flag; both return trace so they chain. |
trace.sink |
function or null (default). Receives output instead of console.log. |
trace.version |
Version string. |
One file, three ways in:
<!-- Browser global -->
<script src="trace.js"></script>
<script>trace.on();</script>// CommonJS
const trace = require('./trace.js');
// Node ESM — resolves through the CommonJS default export
import trace from './trace.js';In a browser <script type="module">, import the file for its side effect and take the global —
trace.js deliberately has no export statement so that a single file can serve every context:
import './trace.js';
const { trace } = globalThis;There is nothing here you couldn't write yourself in ten minutes. The Error().stack trick is
well known and about as old as console.log. The value on offer is the packaging: the flag read
live rather than captured at load, both stack formats handled, the try/catch that means it
can't take your app down, an (anon) fallback instead of an exception, and a default of off so
it's safe to leave in place.
It's free. Take it, copy it, rename it, ship it.
MIT — see LICENSE. Copyright © 2026 James Robinson / 1978.io.
A 1978.io project · github.com/1978io/trace