On This Page
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 usagelog('Application started', 'info');log('Debug information', 'debug');
// With namespace and datalog('User login successful', 'info', { namespace: 'UserService', data: [{ userId: 123, timestamp: new Date() }]});
// JSON format for structured logging — one JSON line per recordlog('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 stylinglog('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 connectedwarn('reconnecting', { data: { attempt: 2 } });logError('socket closed unexpectedly'); // console.error, nothing thrown
// the coded family still owns real refusalserror('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 httpwarnOnce('plainCookie', 'the session cookie is set over plain http'); // silenterrorOnce('plainCookie', 'seen at warn is seen at error'); // silent
// once per entityfor (const collection of ['todos', 'todos', 'posts']) { warnOnce(`search:${collection}`, `"${collection}" falls back to the reference floor`);}
// a key alone is its own messagewarnOnce('booted from the in-memory adapter');
// the guard at the callsite folds the call and the message out of productionisDevelopment && 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 continueerror('forbidden', 'todos:secret.field', { explanation: isDevelopment ? 'the field is private — write the fields you mean' : 0, detail: { collection: 'todos', field: 'secret.field' },});
// refuse outrightthrowError('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 thereglobalThis.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 reportconst 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 logconst [, 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.