This page documents the 0.4 release candidates of @nexusdi/core. The newest on npm is 0.4.0-rc.1. The documentation for 0.3.2, the current release, is at nexus.js.org. Try the release candidate.
Error Text Packs
By default, NexusDI provides concise, one-line error messages. While efficient for developers, these can be too terse for some environments. Text Packs allow you to replace these codes with full, human-readable explanations and suggested fixes. A message holds the code and the error’s fields, then the link to the code’s page.
errors({ text }) from @nexusdi/errors reads the packs. Install it from the next tag:
npm install @nexusdi/errors@nextHow Text Packs Work
A text pack is a mapping of error codes to formatting functions. When the @nexusdi/errors plugin is registered, it reads these packs in order. The first pack that returns a message for a given code wins. A pack holds the text for the codes one package raises. The plugin reads your packs in array order, then core’s pack. A pack listed earlier wins for the same code, and a pack that rewords core’s text works by being in the list.
Using Official Text Packs
First-party packages provide their own text packs. Each sits at a ./text entry: interceptorsText at @nexusdi/interceptors/text, and federationText at @nexusdi/federation/text. Core’s pack, coreText at @nexusdi/core/text, is part of every errors(), so core’s codes need no pack in the list. A code no pack covers keeps its one-line message. To enable them, pass them to the errors() plugin:
import { Nexus, Token, defineModule, isNexusError } from '@nexusdi/core';
import { provide } from '@nexusdi/core';
import type { NexusPlugin } from '@nexusdi/core';
import { errors } from '@nexusdi/errors';
import { interceptor, interceptors } from '@nexusdi/interceptors';
import type { CallContext, Interceptor, Next } from '@nexusdi/interceptors';
import { interceptorsText } from '@nexusdi/interceptors/text';
interface IShipComputer {
status(): string;
}
const COMPUTER = new Token<IShipComputer>('ShipComputer');
const AUDIT = new Token<Interceptor>('Audit');
const TRACE = new Token<Interceptor>('Trace');
class TraceInterceptor implements Interceptor {
intercept(_call: CallContext, next: Next) {
return next();
}
}
class QuantumComputer implements IShipComputer {
status() {
return 'online';
}
}
const Engineering = defineModule({
name: 'Engineering',
providers: [provide(COMPUTER, { useClass: QuantumComputer })],
exports: [COMPUTER],
});
async function firstMessage(textPlugin: NexusPlugin) {
const error = await Nexus.create(Engineering, {
plugins: [
textPlugin,
interceptors({
register: [interceptor(TRACE, { useClass: TraceInterceptor })],
global: [AUDIT],
}),
],
}).catch((caught: unknown) => caught);
return isNexusError(error, 'NEXUS_BLUEPRINT_INVALID')
? (error.errors[0]?.message ?? '')
: '';
}
const thin = await firstMessage(errors()); // -> '[NEXUS_INTERCEPTOR_MISSING] token=Audit. https://nexus.js.org/errors/NEXUS_INTERCEPTOR_MISSING'
console.log(thin);
const full = await firstMessage(errors({ text: [interceptorsText] })); // -> '[NEXUS_INTERCEPTOR_MISSING] a global entry or binding uses the interceptor Audit, which is not registered.\n Fix: add Audit to interceptors({ register }).'
console.log(full);Nothing in a package’s main entry imports its ./text entry, so an app that leaves a pack out carries none of it. Keep one list of packs and pass it to every plugin that writes text:
Not run
const text = [interceptorsText, federationText];
const plugins = development ? [devtools({ text })] : [errors({ text })];Try it:
- Run
npm install @nexusdi/interceptors@next, save the region astext.ts, and runnpx tsx text.ts. The first line is the one-line message, the next two the full text. - Change
errors({ text: [interceptorsText] })in the second call toerrors()and run again. Both messages are now the one-line form, with the link to NEXUS_INTERCEPTOR_MISSING. - Put
{ text: [interceptorsText] }back and run again. The full text with itsFix:line prints once more.
Customizing Error Messages
You can create your own text pack to reword core errors or provide translations. Because text packs are just functions, you can use the error’s internal fields (like requester or token) to build dynamic messages. This pack translates NEXUS_MISSING_PROVIDER into Swedish:
import { Nexus, Token, defineModule, isNexusError } from '@nexusdi/core';
import { provide } from '@nexusdi/core';
import type { ErrorTextPack } from '@nexusdi/core';
import { errors } from '@nexusdi/errors';
interface INavCharts {
plot(to: string): string;
}
interface IShipComputer {
course(to: string): string;
}
const NAV_CHARTS = new Token<INavCharts>('NavCharts');
const COMPUTER = new Token<IShipComputer>('ShipComputer');
class QuantumComputer implements IShipComputer {
static deps = [NAV_CHARTS] as const;
constructor(private readonly charts: INavCharts) {}
course(to: string) {
return this.charts.plot(to);
}
}
const Engineering = defineModule({
name: 'Engineering',
providers: [provide(COMPUTER, { useClass: QuantumComputer })],
});
const swedish = {
NEXUS_MISSING_PROVIDER: (error) => ({
message: `${error.requester} i ${error.module} behöver ${error.token}, men ingen modul tillhandahåller den.`,
fix: `tillhandahåll ${error.token} i ${error.module} eller i en modul som ${error.module} importerar.`,
}),
} satisfies ErrorTextPack;
const error = await Nexus.create(Engineering, {
plugins: [errors({ text: [swedish] })],
}).catch((caught: unknown) => caught);
const inner = isNexusError(error, 'NEXUS_BLUEPRINT_INVALID')
? error.errors[0]
: null;
const message = inner?.message; // -> '[NEXUS_MISSING_PROVIDER] ShipComputer i Engineering behöver NavCharts, men ingen modul tillhandahåller den.\n Fix: tillhandahåll NavCharts i Engineering eller i en modul som Engineering importerar.'
console.log(message);Implementation Rules:
- Type Safety: Use the
satisfies ErrorTextPackhelper to ensure your pack matches the required error classes. The helper types each entry with the error class of its code. - Fallbacks: If your function returns
undefined, NexusDI moves to the next pack in the list.
Try it:
- Save the region as
translate.tsand runnpx tsx translate.ts. The message and itsFix:line print in Swedish. - Change
error.requestertoerror.requestorin themessageline.npx tsc --noEmitflags the line withProperty 'requestor' does not exist on type 'MissingProviderError'. Did you mean 'requester'?, andnpx tsx translate.tsstill runs and printsundefined i Engineeringwhere the requester belongs. - Change it back to
error.requester. The line printsShipComputer i Engineeringagain. - Change
errors({ text: [swedish] })toerrors(). The message is core’s English text, and theFix:line readsprovide NavCharts in Engineering or in a module Engineering imports.
Explaining Logged Errors
Sometimes you encounter an error in a log file where the errors() plugin wasn’t active. You can use the explain() utility to retroactively apply a text pack to a logged error. The same works in a test. explain(error, { view, text }) reads text in the order errors() does, and returns undefined for a code no pack covers.
import { Nexus, Token, defineModule, isNexusError } from '@nexusdi/core';
import { provide } from '@nexusdi/core';
import { explain } from '@nexusdi/errors';
interface INavCharts {
plot(to: string): string;
}
interface IShipComputer {
course(to: string): string;
}
const NAV_CHARTS = new Token<INavCharts>('NavCharts');
const COMPUTER = new Token<IShipComputer>('ShipComputer');
class QuantumComputer implements IShipComputer {
static deps = [NAV_CHARTS] as const;
constructor(private readonly charts: INavCharts) {}
course(to: string) {
return this.charts.plot(to);
}
}
const Engineering = defineModule({
name: 'Engineering',
providers: [provide(COMPUTER, { useClass: QuantumComputer })],
});
const error = await Nexus.create(Engineering).catch(
(caught: unknown) => caught,
);
const inner = isNexusError(error, 'NEXUS_BLUEPRINT_INVALID')
? error.errors[0]
: undefined;
const logged = inner?.message; // -> '[NEXUS_MISSING_PROVIDER] token=NavCharts requester=ShipComputer module=Engineering. https://nexus.js.org/errors/NEXUS_MISSING_PROVIDER'
console.log(logged);
const text = inner === undefined ? undefined : explain(inner);
const full = text?.message; // -> 'ShipComputer (module Engineering) depends on NavCharts, but no provider of NavCharts is visible in Engineering.\n Fix: provide NavCharts in Engineering or in a module Engineering imports.'
console.log(full);Without the container’s view, explain() has no near misses to suggest. errors() passes the view, so a container with the plugin adds lines such as “NavCharts is provided in Tactical, which does not export it.” when Engineering imports a Tactical module that provides NavCharts and leaves it out of its exports.
Try it: In the region, import errors from @nexusdi/errors and pass { plugins: [errors()] } to Nexus.create. logged is now the full text, which starts [NEXUS_MISSING_PROVIDER] ShipComputer (module Engineering) depends on NavCharts, and explain(inner) prints the same text without the [NEXUS_MISSING_PROVIDER] prefix. Remove the option again and logged returns to the one-line form.
Integration with DevTools
The @nexusdi/devtools plugin also accepts text packs. Pass them as devtools({ text }) or inspect(root, { text }), and the errors that devtools() and inspect() report are formatted using the same human-readable text as your production application. Both read the packs in the order errors() does. inspect() adds its own formatter after the plugins you pass, so an errors() in your plugins words an error first.
import { Nexus, Token, defineModule, isNexusError } from '@nexusdi/core';
import { provide } from '@nexusdi/core';
import { devtools, inspect } from '@nexusdi/devtools';
import { interceptor, interceptors } from '@nexusdi/interceptors';
import type { CallContext, Interceptor, Next } from '@nexusdi/interceptors';
import { interceptorsText } from '@nexusdi/interceptors/text';
interface IShipComputer {
status(): string;
}
const COMPUTER = new Token<IShipComputer>('ShipComputer');
const AUDIT = new Token<Interceptor>('Audit');
const TRACE = new Token<Interceptor>('Trace');
class TraceInterceptor implements Interceptor {
intercept(_call: CallContext, next: Next) {
return next();
}
}
class QuantumComputer implements IShipComputer {
status() {
return 'online';
}
}
const Engineering = defineModule({
name: 'Engineering',
providers: [provide(COMPUTER, { useClass: QuantumComputer })],
exports: [COMPUTER],
});
const text = [interceptorsText];
const audit = () =>
interceptors({
register: [interceptor(TRACE, { useClass: TraceInterceptor })],
global: [AUDIT],
});
const firstLine = (error: unknown) =>
isNexusError(error, 'NEXUS_BLUEPRINT_INVALID')
? error.errors[0]?.message.split('\n')[0]
: undefined;
const created = await Nexus.create(Engineering, {
plugins: [devtools({ text }), audit()],
}).catch((caught: unknown) => caught);
const fromDevtools = firstLine(created); // -> '[NEXUS_INTERCEPTOR_MISSING] a global entry or binding uses the interceptor Audit, which is not registered.'
console.log(fromDevtools);
let checked: unknown;
try {
inspect(Engineering, { text, plugins: [audit()] });
} catch (caught) {
checked = caught;
}
const fromInspect = firstLine(checked); // -> '[NEXUS_INTERCEPTOR_MISSING] a global entry or binding uses the interceptor Audit, which is not registered.'
console.log(fromInspect);Try it: Run npm install @nexusdi/devtools@next, then change devtools({ text }) to devtools() and inspect(Engineering, { text, plugins: [audit()] }) to inspect(Engineering, { plugins: [audit()] }). Both lines print the one-line form, [NEXUS_INTERCEPTOR_MISSING] token=Audit. and the link. Restore text in both and the full sentence returns.
nexusdi graph from @nexusdi/cli takes no packs of its own. It reads the --plugins array, so a list that holds errors({ text }) words the errors the command prints (Visualizing Your Dependency Graph).
Errors That Carry Their Own Text
Some errors are raised where no container formats them, and their package writes the full text into the error itself:
- a
@UseInterceptorsfault at class definition NEXUS_LEGACY_DECORATORSNEXUS_INTERCEPTOR_NOT_READYfor a call to an intercepted method before its interceptors exist- the two override errors of
@nexusdi/testing
Such an error reads the same with or without errors(), and a pack cannot replace its text:
import { Token, defineModule, isNexusError, provide } from '@nexusdi/core';
import { createTestingContainer } from '@nexusdi/testing';
interface IReactorCore {
readonly output: number;
}
const REACTOR = new Token<IReactorCore>('ReactorCore');
const SHIELD_GRID = new Token<{ readonly strength: number }>('ShieldGrid');
class FusionReactor implements IReactorCore {
readonly output = 1.21;
}
const Engineering = defineModule({
name: 'Engineering',
providers: [provide(REACTOR, { useClass: FusionReactor })],
});
const error = await createTestingContainer(Engineering)
.override(SHIELD_GRID, { useValue: { strength: 1 } })
.create()
.catch((caught: unknown) => caught);
const inner = isNexusError(error, 'NEXUS_BLUEPRINT_INVALID')
? error.errors[0]
: null;
const message = inner?.message; // -> '[NEXUS_OVERRIDE_UNUSED] override(ShieldGrid) matched no provider in the module graph.\n Fix: remove the override, or import the module that provides ShieldGrid.'
console.log(message);Try it: Run npm install @nexusdi/testing@next, run the region, then import errors from @nexusdi/errors, pass { plugins: [errors()] } to create(), and run it. The message of NEXUS_OVERRIDE_UNUSED is the same two lines as before. A pack with a NEXUS_OVERRIDE_UNUSED entry in errors({ text }) leaves them unchanged too.