Skip to content

Error Handling

Every failed snaptrude.* call rejects with a typed PluginError carrying a stable string code, structured details, and — where the host can infer one — a machine-actionable hint. Branch on code, not on message text: messages may change, codes never do.

ts
import { snaptrude, PluginError } from "@snaptrude/plugin-client";

try {
  const result = await snaptrude.design.boolean.union(solids);
} catch (e) {
  if (!PluginError.is(e)) throw e; // not an API failure — rethrow
  switch (e.code) {
    case "VALIDATION":
      console.log(e.details?.issues, e.hint); // which argument, what was wrong
      break;
    case "RATE_LIMITED":
      await sleep(e.details?.retryAfterMs as number);
      // retry the call
      break;
    case "HANDLE_INVALID":
      // e.handle names the stale id — re-query instead of using cached handles
      break;
    default:
      console.error(`${e.code} [${e.errorId}]: ${e.message}`);
      throw e;
  }
}

Use PluginError.is(e), not instanceof. If your bundle ships its own copy of the SDK, instanceof breaks across the two copies; the brand check always works. Category subclasses (PluginValidationError, PluginQuotaError, …) are exported for convenience and work under a single SDK copy.

The PluginError fields

FieldTypeMeaning
codestringStable machine-readable code — the branching key (table below)
messagestringHuman-readable description. For INTERNAL this is a generic reference — see below
errorIdstringPer-occurrence id correlating your error with the host-side log entry. Include it in bug reports
detailsobject?Code-specific structured data — issues, retryAfterMs, didYouMean, handles, …
handlestring?The primary offending handle id, when the failure is handle-scoped
details.handlesstring[]?ALL offending handle ids for multi-input operations (always ids you passed in)
methodPathstring?The dotted API path that failed, e.g. "design.boolean.union"
hintstring?One actionable sentence on how to fix the call
categorystringDerived family of the code (validation, quota, timeout, …)

Error codes

Codes are a closed, append-only union — new codes may appear in future hosts (handle unknown codes with a default: branch), existing ones never change meaning.

Your call was wrong

CodeWhenAct on
VALIDATIONAn argument failed schema validationdetails.issues[]{path, code, message} per problem
INVALID_RPC_FORMATThe RPC payload itself was malformed (usually an SDK/version problem)
METHOD_NOT_FOUNDNo such API methoddetails.didYouMean — closest real method
HANDLE_INVALIDA handle could not be resolved — not found, disposed, or not yours. Deliberately ONE code for all resolution failureshandle, re-query the entity
HANDLE_KIND_MISMATCHReserved. Today a wrong-kind handle also reports HANDLE_INVALIDhandle
PRECONDITION_FAILEDThe call is valid but the model/state doesn't allow it (unknown storey, mesh-less component, locked underlay, …)details names the violated condition; hint says what to establish first

The environment said no

CodeWhenAct on
METHOD_NOT_PERMITTEDThe API is disabled for this plugin (e.g. write APIs off)
RATE_LIMITEDToo many callswait details.retryAfterMs, then retry
RESOURCE_QUOTA_EXCEEDEDHandle arena fullrelease handles (core.handles.release, scopes, await using)
TIMEOUTThe host bounded your call's execution timeretry once; if persistent, reduce the work per call

The operation failed

CodeWhenAct on
OPERATION_FAILEDThe engine could not perform a valid request (CSG failed, placement blocked, …)details.reason / details.engineCode where available
RESULT_NOT_SERIALIZABLEThe method result could not cross the worker boundaryreport with errorId — usually a host bug
RESULT_TOO_LARGEResult exceeded transport limitsnarrow the query
BATCH_PARTIALReserved: a batch completed partially (per-item outcomes on the success channel)

Infrastructure

CodeWhenAct on
TRANSPORT_LOSTThe host connection died mid-callnot retryable — the plugin is being torn down
PLUGIN_NOT_READY / PLUGIN_TERMINATEDLifecycle states
CALL_TIMEOUTReserved for a client-side deadline
INTERNALUnexpected host-side faultsee below
UNKNOWNThe host predates structured errors (legacy fallback)

INTERNAL errors and errorId

When something unexpected breaks inside the host, you get a deliberately generic message:

Internal error. Reference: 3f2a9c1e-…

The real failure (message, stack, method, plugin) is logged host-side under that errorId. Nothing about host internals crosses into the plugin sandbox. Include the errorId when reporting the problem — it is the lookup key for the actual cause.

Retry guidance

Only two codes are worth an automatic retry: RATE_LIMITED (after details.retryAfterMs) and TIMEOUT (once). Everything else is deterministic — the same call will fail the same way until you change the call or the model state.

Legacy note

Hosts also populate a legacy error string alongside the structured envelope, and old plugins that string-match it keep working. New code should never parse messages — branch on code.

Per-API errors

Each namespace page documents its specific error conditions in an Errors section — which codes it throws, when, and what lands in details.