Debug UtilitiesAPI reference for logging and coded error handling utility functionsbugAPI Reference
Categories

Debug Utilities

The Debug utilities provide the two reporting doors a library needs: log for styled console output — with createLogger binding it to a namespace once — and createErrors for coded errors that lead with one uniform, greppable line and carry a teaching explanation beneath it in development.

Functions

log

function log(message, level = 'log', { namespace = '', data = undefined, color = 'inherit', timestamp = false, format = 'standard', consoleMethod = null, silent = false, title = namespace, showTitle = true, titleColor = null, noColor = isServer } = {})

A flexible logging utility with formatting, namespacing, and multiple output options. Supports colored output, timestamps, structured JSON format, and configurable titles. Styling is browser-only by default — noColor defaults to isServer, so server output is plain text with no %c directives or style arguments.

Parameters

Name Type Description
message string The message to log
level string Log level (‘debug’, ‘log’, ‘info’, ‘warn’, ‘error’)
options object Optional configuration
Options
Name Type Default Description
namespace string ‘’ Namespace for grouping related logs — prints verbatim as the default title
data any undefined Additional data to include with the log message. Accepts arrays and objects
color string ‘inherit’ Text color for the message
timestamp boolean false Whether to include timestamp in the output
format string ‘standard’ Output format (‘standard’ or ‘json’ — json emits each record as one JSON-serialized line)
consoleMethod string null Override the console method used for output
silent boolean false Suppress all output when true
title string namespace Title/label to display before the message
showTitle boolean true Whether to show the title/label
titleColor string null Color for the title/label (defaults to level color)
noColor boolean isServer Skip the %c style directives and style arguments — plain text, title still present. Pass false to force styling on the server

Returns

This function does not return a value.

Example

import { log } from '@semantic-ui/utils';
// Basic usage
log('Application started', 'info');
log('Debug information', 'debug');
// With namespace and data
log('User login successful', 'info', {
namespace: 'UserService',
data: [{ userId: 123, timestamp: new Date() }]
});
// JSON format for structured logging — one JSON line per record
log('API response received', 'info', {
format: 'json',
namespace: 'ApiClient',
timestamp: true,
data: [{ status: 200, endpoint: '/api/users' }]
});
// {"timestamp":"2026-08-12T10:24:00.000Z","level":"info","namespace":"ApiClient","message":"API response received","data":[...]}
// Custom styling
log('Important notice', 'warn', {
title: 'NOTICE',
titleColor: '#FF6B35',
timestamp: true
});
// Force plain output anywhere (the server default)
log('Job finished', 'info', { namespace: 'worker', noColor: true });

createLogger

function createLogger({ namespace = '', timestamp = false, ...defaults } = {})

Binds log to shared defaults once and returns the narration bundle — flat functions made for destructuring. Every member calls log with the factory defaults shallow-merged beneath the callsite options, so a call site can still override anything. The four level wrappers absorb the level slot only: info(message, options) is log(message, 'info', options) with the defaults underneath. The bound log keeps its level parameter for the occasional dynamic level.

The namespace prints verbatim — db shows as db, never Db. Granularity belongs in the value itself: createLogger({ namespace: 'physics:collision' }).

Parameters

Name Type Description
defaults object Any log option — namespace, timestamp, format, titleColor, and the rest — applied to every call from the bundle

Returns

{ log, debug, info, warn, error, logOnce, debugOnce, infoOnce, warnOnce, errorOnce } — flat bound functions, each level member with its once form.

Name Description
log(message, level, options) log with the defaults merged in, level slot intact
debug(message, options) Logs at debug level
info(message, options) Logs at info level
warn(message, options) Logs at warn level
error(message, options) Logs at error level
warnOnce(key, message, options) Logs at warn level the first time this logger sees key, silent after
debugOnce, infoOnce, errorOnce The same once form at the other levels, one memory shared by all
logOnce(key, message, level, options) The bound log’s once form, level slot intact

Example

Both debug families export an error. A file that binds both renames one at the destructure — the consumer picks the name, no third export needed:

import { createLogger, createErrors, isDevelopment } from '@semantic-ui/utils';
const { info, warn, error: logError } = createLogger({ namespace: 'sync' });
const { error, throwError } = createErrors({ namespace: 'sync' });
info('connected'); // sync connected
warn('reconnecting', { data: { attempt: 2 } });
logError('socket closed unexpectedly'); // console.error, nothing thrown
// the coded family still owns real refusals
error('writeRejected', 'todos:a7f2', {
explanation: isDevelopment ? 'the server refused the write' : 0,
});

warnOnce

warnOnce(key, message = key, options)

Returned by createLogger beside debugOnce, infoOnce, errorOnce, and logOnce: the once form of every level member. The first call with a key prints exactly what the plain member prints, and every later call with that key on the same logger is silent, whatever its message. The key is a word for the condition ('plainCookie'), or a template of stable parts for a condition per entity (`liveDep:${publication}:${collection}`), never the message text: a count or an id in the key would make every call a new condition.

One memory serves the whole family, so a condition seen at warn is seen at error. There is no reset: a fresh logger is the clean slate. The memory is unbounded, so key on things of bounded cardinality (a collection, a role, a route pattern) and never on a request id.

Development advice keeps its guard at the callsite, isDevelopment && warnOnce(...), so a bundler folds the call and its message string out of a production build together (see Development Messages). An operational line that must reach production calls it bare.

Parameters

Name Type Description
key string The condition’s name, the identity the once-ness keys on
message string The line to print. Defaults to the key, so a one-argument call dedupes on its own text
options object Any log option, merged over the logger’s defaults exactly as the plain member merges them

logOnce(key, message = key, level, options) keeps the bound log’s level slot, with the key in front.

Returns

This function does not return a value.

Example

import { createLogger, isDevelopment } from '@semantic-ui/utils';
const { warnOnce, errorOnce } = createLogger({ namespace: 'sync' });
warnOnce('plainCookie', 'the session cookie is set over plain http'); // sync the session cookie is set over plain http
warnOnce('plainCookie', 'the session cookie is set over plain http'); // silent
errorOnce('plainCookie', 'seen at warn is seen at error'); // silent
// once per entity
for (const collection of ['todos', 'todos', 'posts']) {
warnOnce(`search:${collection}`, `"${collection}" falls back to the reference floor`);
}
// a key alone is its own message
warnOnce('booted from the in-memory adapter');
// the guard at the callsite folds the call and the message out of production
isDevelopment && warnOnce('unmounted', 'no accounts key was named, so the accounts plane did not mount');

createErrors

function createErrors({ namespace = '', ...defaults } = {})

The binder over error and throwError, mirroring createLogger over log: pre-fills the options into every call (callsite options win) and returns the bound pair. Bind it once per namespace and every refusal raised under it reads the same way: a stable code you can route on, an address (at) that says where it happened — both riding the error as properties — and a development explanation that folds out of production builds.

Every message leads with one uniform line, <namespace> <verb> [<code>] <at>, parseable with /^(\S+) (\S+) \[([\w-]+)\] (.*)$/m — so a support ticket carrying a pasted error still names the namespace, the code, and the address. The verb gives every line a searchable classification surface: a stable token position carrying your own taxonomy of error, whatever it is. Grep a word and you have filtered a console by class; the pattern’s verb group gives the same axis programmatically. The vocabulary is yours — any word renders, a game engine might say despawned where a payments library says declined — and refused is the default when none is given. In development the explanation follows on its own line; in production the line is the whole message.

Parameters

Name Type Description
options object The namespace binding
Options
Name Type Default Description
namespace string '' The display identity leading the production line (e.g. sync, component)
…defaults object Any error option to pre-fill — ErrorClass, a default verb, whatever every call should carry

Returns

{ error, throwError } — the reporting door and the throwing door, both bound to the namespace.

Name Returns Description
error(code, at, options) Error Reports through the error channel and lets the caller continue
error.line(code, at) string The production line alone, for console seats
throwError(code, at, options) never Throws synchronously

Example

import { createErrors, isDevelopment } from '@semantic-ui/utils';
const { error, throwError } = createErrors({ namespace: 'sync' });
// report and continue
error('forbidden', 'todos:secret.field', {
explanation: isDevelopment ? 'the field is private — write the fields you mean' : 0,
detail: { collection: 'todos', field: 'secret.field' },
});
// refuse outright
throwError('unknownCollection', 'invoices', {
explanation: isDevelopment ? 'no collection named invoices is registered on this client' : 0,
});

error

error(code, at, { namespace, explanation, detail, verb, report, ErrorClass } = {})

Exported top-level, and returned bound by createErrors with the options pre-filled. Reports a coded error through the error channel without interrupting the caller — log’s sibling for things that went wrong. The raise is asynchronous, so the current call stack completes first: it routes to globalThis.onError when an app installs one, and falls back to console.error otherwise, so a report is never silently lost.

Sometimes the error is a value — handed to a promise rejection, attached to a result — and reporting is someone else’s job. report: false builds and returns the same coded error without reporting it, making error the family’s builder without a third function.

On engines with Error.captureStackTrace (V8 among them) the stack opens at your callsite, with the library’s own construction frames trimmed — other engines keep the full stack.

Parameters

Name Type Description
code string A stable identifier for this refusal, routable in code. Never rephrase one in place — the code is the contract, the prose is not
at string Where it happened: the greppable address, like todos:secret.field or db-1 -> db-2, riding the error as error.at. Say why in the explanation
options object Optional configuration
Options
Name Type Default Description
namespace string '' The display identity leading the line — empty composes the line without a leading token. The bound form pre-fills it
explanation string | 0 undefined The development teaching message. When present it follows the production line on its own line; when folded away the line is the whole message. See Development Messages
detail any undefined Structured data attached to the error as error.detail
verb string 'refused' The line’s verb — any word, your classification of the line. Nullish falls back to 'refused'
report boolean true Set false to build and return the coded error without reporting it — no onError routing, no console fallback
ErrorClass function Error The constructor to build with

Returns

The Error it reported, carrying code, at, and (when given) detail. Returning it lets a caller attach it to local state or a result envelope without building a second one.

Example

import { createErrors, isDevelopment } from '@semantic-ui/utils';
const { error } = createErrors({ namespace: 'sync' });
// an app that installs a channel gets the report there
globalThis.onError = (err) => reportToService(err.code, err.message, err.detail);
const reported = error('writeRejected', 'todos:a7f2', {
explanation: isDevelopment ? 'the server refused the write, the optimistic value was rolled back' : 0,
detail: { docId: 'a7f2', reason: 'version-conflict' },
});
console.log(reported.code); // 'writeRejected'
console.log(reported.at); // 'todos:a7f2'
console.log(reported.message); // 'sync refused [writeRejected] todos:a7f2',
// with the explanation beneath it in development
// the error as a value — built, never reported: the rejection is the report
const timeout = new Promise((resolve, reject) => {
setTimeout(() => reject(error('timedOut', 'todos:pull', { report: false })), 5000);
});

throwError

throwError(code, at, { namespace, explanation, detail, verb, ErrorClass } = {})

Exported top-level, and returned bound by createErrors with the options pre-filled. Same arguments as error, thrown synchronously — for the refusal that must stop the caller where it stands.

Parameters

Identical to error, minus report — the throw is the delivery.

Returns

Never returns.

Example

import { createErrors, isDevelopment } from '@semantic-ui/utils';
const { throwError } = createErrors({ namespace: 'schema', ErrorClass: TypeError });
const parseField = (name, spec) => {
if (!spec.type) {
throwError('missingType', `fields.${name}`, {
explanation: isDevelopment ? `field "${name}" has no type — every field declares one` : 0,
detail: { field: name },
});
}
return spec;
};

error.line

error.line(code, at, { namespace, verb } = {})

Renders the production line by itself, without building or reporting an Error — for console seats and log messages that want the same greppable shape as a real refusal. Rides the top-level error export and the bound form alike; the bound form pre-fills the namespace.

Parameters

Name Type Description
code string The refusal code
at string The address it happened at
options object Optional configuration — verb sets the line’s verb, any word, defaulting to 'refused'; namespace sets the leading token

Returns

The string <namespace> <verb> [<code>] <at>, or <verb> [<code>] <at> when there is no namespace.

Example

import { createErrors } from '@semantic-ui/utils';
const { error } = createErrors({ namespace: 'sync' });
console.warn(error.line('storageChanged', 'db-1 -> db-2'));
// 'sync refused [storageChanged] db-1 -> db-2'
console.info(error.line('cursorReset', 'todos@41', { verb: 'recovered' }));
// 'sync recovered [cursorReset] todos@41'
// the same line parses back out of a pasted log
const [, namespace, verb, code, at] = 'sync refused [storageChanged] db-1 -> db-2'
.match(/^(\S+) (\S+) \[([\w-]+)\] (.*)$/m);

Development Messages

explanation is teaching prose. In development it rides beneath the production line — the message still leads with the greppable address, so the explanation never needs to restate where it happened. It belongs in a development build and nowhere else, so fold it at the callsite, in argument position:

error('forbidden', 'todos:secret.field', {
explanation: isDevelopment ? 'the field is private — write the fields you mean' : 0,
});

The reason the ternary has to sit there, and not one layer in: a bundler define folds branches, never arguments. isDevelopment (and __DEV__, and process.env.NODE_ENV) collapses an if or a ?: at build time, but a string literal sitting in argument position is just a value being passed — it ships in production whole, even when the callee never reads it. The ternary must live where the string does.

Deferring the prose behind a helper or a thunk does not recover it. Measured on a real corpus: moving explanations behind a helper reclaimed 37 bytes of a ~2KB pool, while the inline callsite ternary reclaimed all of it.

Everything structural — code, at, detail — is cheap and belongs in every build. Those are what production debugging runs on. Only the prose folds.

The same rule governs tooltipText on the timeline utilities, and isDevelopment is the define to fold against.

Previous
Dates
Next
Environment