Most of us have typed some version of this:
console.log('Called:', arguments.callee.name); // don't
Two 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.
Install
Drop the file in
<script src="/js/trace.js"></script>
<script>
trace.enabled = true; // see "Enabling it" below
</script>
This is the supported route. trace.js
defines a global trace; load it before the code you want to trace.
Or a module
const trace = require('@1978io/trace'); // CommonJS
import trace from '@1978io/trace'; // Node ESM
Or just paste the thirty lines into your own utilities bundle — the licence allows it and nobody will mind.
Live demo
Flip the switch, then call something
Open your devtools console first — that is where trace()
normally writes. The panel below mirrors it via trace.sink so you can see
it either way. With tracing off, the buttons log nothing at all.
trace.enabled === false
The flag is read on every call, so you can type trace.enabled = true straight
into the devtools console of this page and the next call traces. No reload, no rebuild.
The switch above will notice and follow.
The functions those buttons call
function saveDraft() {
trace(); // → Called: saveDraft
}
function loadUser(id) {
trace(id); // → Called: loadUser 7
}
function applyDiscount(code) {
const total = 42.5;
trace({ code, total }); // → Called: applyDiscount { code: 'SPRING', total: 42.5 }
} // the object shorthand keeps the variable names
[1].forEach(function () {
trace('nothing to name here'); // → Called: (anon) nothing to name here
}); // an anonymous callback falls back, it never throws
Documentation
Usage
Bare — just the caller's name
function saveDraft() {
trace();
}
saveDraft();
// Called: saveDraft
With values — anything you pass is appended
function loadUser(id) {
trace(id);
}
loadUser(7);
// Called: loadUser 7
Arguments are passed straight through to console.log, so objects, arrays and DOM nodes stay
inspectable in devtools rather than being stringified.
The trace({ myVar }) shorthand — names and values
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.
Enabling it
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 flag
The 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.
Auto-enabling in development
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;
🔴 Don't sniff the hostname
The tempting one-liner:
trace.enabled = location.hostname === 'localhost'; // ✗ don't
It 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.
Silent in production
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.
How the name detection works
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 here
Two 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.
Custom sink
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.
API
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. |
Module formats
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;
Honestly
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.