Skip to content

Error codes

Every structured error Dougong throws carries a stable code string. Application code should branch on error.code rather than matching message text — messages change, codes do not.

The naming rule

A code names which object's invariant was violated, instead of vaguely saying "a plugin failed":

PrefixThe object whose invariant brokePackage
SERVICE_* / CONTRACT_* / CONFIG_*Contract identity, the dependency graph, a config declarationCore
LIFETIME_*structured resource ownershipCore
INSTALLATION_*One existing installationCore
GROUP_*One installation-ownership subtreeCore
ARTIFACT_*One external artifact that disagrees with itselfPlatform
REGISTRATION_*Registration identity and the registration dependency graph declared by manifestsPlatform
MANIFEST_* / MODULE_* / API_* / PERMISSION_* / PLATFORM_*The trust and loading boundariesPlatform

So INSTALLATION_REMOVED is a Core installation and REGISTRATION_REMOVED is a Platform registration — no need to open the implementation to learn which layer you are in.

Error types

ts
class DougongError extends Error {
  readonly code: string
}

class ConfigValidationError extends DougongError {   // code: "CONFIG_INVALID"
  readonly issues: ReadonlyArray<StandardSchemaV1.Issue>
}

class PlatformError extends DougongError {}

class PermissionDeniedError extends PlatformError {  // code: "PERMISSION_DENIED"
  readonly manifestName: string
  readonly denied: ReadonlyArray<string>
}

Several failures are aggregated into a standard AggregateError, with every cause preserved in errors.

TypeError versus Error

Beyond coded errors, Dougong uses two native types:

  • TypeError — the caller passed the wrong thing (not a function, not a Contract, a key conflict, or continued using an object whose local capability was revoked)
  • Error — an internal invariant broke; that is a framework bug you should never reach in normal use

So you can branch by constructor: a DougongError is an expected operational failure, while a TypeError is a usage problem.

Core (@dougongjs/core)

Graph construction

These are thrown before any Instance starts. The execution graph has not moved.

CodeTrigger
SERVICE_CYCLEA dependency cycle. The message carries the real path: app.a:1 -> app.b:2 -> app.a:1. A plugin requiring a Service it provides counts too
SERVICE_CONFLICTTwo plugins provide the same Service
SERVICE_MISSINGA required Service has no provider (an optional() declaration does not count)
CONTRACT_CONFLICTThe same Contract ID is used as two different kinds
CONFIG_INVALIDConfig failed Standard Schema validation. error.issues lists the problems per field

Validation precedes shutdown

Every affected Installation's Plugin config is validated in full before any running Instance is stopped. One misspelled field cannot leave the application halfway down.

Startup and active execution

CodeTrigger
SERVICE_NOT_RETURNEDprovides declared a key that the setup return value does not contain
SERVICE_UNAVAILABLEhost.get() was called outside active; or the depended-on Service's installation is not active
LIFETIME_DISPOSEDA Lifetime has begun disposal and therefore rejects new listeners, contributions, tasks, child Lifetimes, cleanups or Event emission
INSTALLATION_UNAVAILABLEThe installation is failed; or setup / a config validator threw a non-Error value (the original is in cause); or an operation ran on an uncommitted draft
INSTALLATION_REMOVEDAn operation on a removed Installation
INSTALLATION_IDENTITYupdate() tried to change the Plugin name. An update may swap implementation and config, never identity

Group

CodeTrigger
GROUP_REMOVEDAn operation on a removed Group, or continued use of a stale ChangeSet created before that Group was removed
GROUP_UNAVAILABLEThe Group was never established; or a Group operation failed with a non-Error value

Platform (@dougongjs/platform)

Trust boundary

Thrown before any external module code is loaded.

CodeTrigger
MANIFEST_INVALIDThe Manifest shape is illegal, or it declares duplicate activation events / permissions
API_INCOMPATIBLEThe Manifest's required apiVersion does not satisfy the application version
PERMISSION_DENIEDThe Authorizer refused. error.denied lists the refused permissions
REGISTRATION_DUPLICATETwo artifacts' manifests map to the same registration identity

Manifest dependency resolution

These errors arise while resolving manifest declarations in the candidate Registration graph. Their codes name the object actually being validated, not a Plugin that may not have loaded yet.

CodeTrigger
REGISTRATION_DEPENDENCY_MISSINGA manifest dependency has no Registration
REGISTRATION_DEPENDENCY_INCOMPATIBLEThe dependency Registration is outside the version range
REGISTRATION_DEPENDENCY_INACTIVEThe dependency Registration exists but is not activated
REGISTRATION_CYCLEManifest dependencies form a cycle in the candidate Registration graph; the message carries the real path

Loading and activation

CodeTrigger
MODULE_LOAD_FAILEDThe loader threw. The original error is in cause
MODULE_INVALIDThe module loaded but exports no valid Plugin
ARTIFACT_IDENTITYOne artifact disagrees with itself: the manifest name differs from the placeholder's or the loaded Plugin's name
REGISTRATION_BUSYThat Registration is changing, or a Platform structural change has closed admission for new root activations
REGISTRATION_UNAVAILABLEThe Registration is unavailable; activation or admission threw a non-Error value (the original is in cause); or an operation ran on an uncommitted registration
REGISTRATION_REMOVEDAn operation on a removed Registration
REGISTRATION_IDENTITYAn update's new artifact carries a different manifest name
PLATFORM_UNAVAILABLEThe Platform is disposed, or in a state that forbids the operation

The three IDENTITY codes

They describe identity invariants on three different objects:

  • INSTALLATION_IDENTITY — an existing Installation tried to change its Plugin name (Core)
  • REGISTRATION_IDENTITY — an existing Registration tried to change its manifest name (Platform)
  • ARTIFACT_IDENTITY — one artifact's manifest disagrees with the Plugin it loads (Platform, where a Registration may not exist yet)

Handling them

Branch on the code

ts
try {
  await installation.ready()
} catch (error) {
  if (!(error instanceof DougongError)) throw error

  switch (error.code) {
    case "CONFIG_INVALID":
      showFieldErrors((error as ConfigValidationError).issues)
      break
    case "SERVICE_MISSING":
      suggestInstallDependency(error.message)
      break
    case "INSTALLATION_UNAVAILABLE":
      offerRetry()
      break
    default:
      report(error)
  }
}

Process aggregate failures

ts
try {
  await host.stop()
} catch (error) {
  if (error instanceof AggregateError) {
    for (const cause of error.errors) report(cause)
  }
}

Receive background errors

Exceptions from background tasks, listeners and diagnostic subscribers never interrupt a Host command; they arrive through the Host's reporting channel:

ts
const host = createHost({
  name: "app",
  onError: (error) => reportToSentry(error),
  logger: myLogger,          // fallback when onError is absent or itself throws
})

The channel is fail-safe: a throwing onError falls back to the logger, and a throwing logger falls silent — observing an error never changes the Host command being observed.

How much a terminal failure retains

Once an Installation detaches from its Host (removed or discarded), it keeps only a plain-data summary: name, message, the minimal constructor category, and code when available. A later read rebuilds the correct DougongError or TypeError; every other failure becomes a plain Error.

The reason is that JavaScript's Error.stack can carry the whole orchestration call frame from where the error was created, letting one historical object keep an entire Host alive.

The normal path is unaffected: a caller awaiting ready() always receives the original Error, and a failed Installation still attached to a live Host keeps its original error too. Only an after-the-fact read of a detached Installation whose caller never awaited ready() gets the summary — and there subclass data such as ConfigValidationError.issues is no longer available.

A terminal Registration follows the same retention rule and records whether a coded error belonged to Core or Platform, so it can rebuild the correct DougongError or PlatformError. It also preserves the caller-error category of TypeError, while subclass-specific fields remain absent from the summary. It never keeps an Installer, Loader or Platform alive merely to preserve a historical stack or cause.

Released under the MIT License.