bits.1978.io

trace()

A tiny JavaScript debug helper. Put trace(); at the top of a function and it logs Called: loadUser — working the name out for itself, off the stack, with no arguments.callee in sight. Silent by default, so it can't ship noise to production.

  • JavaScript
  • ~30 lines
  • zero deps
  • MIT

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
No output yet.

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 as mystery, 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.