Skip to content

Error handling

Errors cross three boundaries in a plugin: inside your backend handler, over the IPC channel to the runtime, and over the SDK channel to your frontend. Each has a typed, code-carrying error so you can branch on what went wrong instead of string-matching messages.

The contract: catch on .code, never on message

Every error the SDK throws is an instance of a typed class with a stable, machine-readable code. Messages are for humans and may be reworded at any time — never branch on them.

ClassPackageThrown by
SdkError@uncorded/plugin-sdkBackend SDK calls (bad args, missing setting, …).
SdkProtocolError (extends SdkError)@uncorded/plugin-sdkA runtime reply that errored or didn't match the expected shape.
PluginError@uncorded/plugin-sdk-frontendFrontend request/handshake failures.
UploadError@uncorded/plugin-sdk-frontendsdk.files.upload failures.
ProxyError@uncorded/plugin-sdk-frontendsdk.proxy.* bootstrap failures.
ts
import { SdkError } from "@uncorded/plugin-sdk";

try {
  await plugin.net.requestPublicPort({ name: "lobby", localPort: 25565, protocol: "tcp" });
} catch (err) {
  if (err instanceof SdkError && err.code === "invalid_argument") {
    // handle a bad request locally
  } else {
    throw err; // don't swallow what you don't recognize
  }
}

Backend: throwing from a handler

A handler that throws rejects that request. The thrown message is delivered to the caller, wrapped by the runtime with the code HANDLER_ERROR. Throw early on bad input — validation is error handling:

ts
plugin.handle("addNote", async (params, user) => {
  const body = typeof params["body"] === "string" ? params["body"].trim() : "";
  if (!body) throw new Error("Note body is required.");      // → caller sees this message
  // …
});

Two error codes the runtime returns before your handler runs, so you can't catch them in the handler — fix the manifest instead:

  • CAPABILITY_DENIED — you called an IPC action whose capability isn't in your manifest permissions. Declare it. (See Permissions.)
  • UNKNOWN_ACTION — the frontend called an action you never registered with plugin.handle. Names must match exactly.

Frontend: handling request failures

sdk.request(action, params) rejects with a PluginError when the backend throws, the call times out (30 s), or the handshake never completed. Show the user something useful and decide whether to retry:

js
try {
  await sdk.request("addNote", { body });
} catch (err) {
  // err.code is stable; err.message is human-readable.
  if (err.code === "REQUEST_TIMEOUT") {
    showToast("The server didn't respond — try again.");
  } else {
    showToast(err.message || "Something went wrong.");
  }
}

Common frontend codes:

CodeMeaningWhat to do
HANDSHAKE_TIMEOUTThe shell never issued a token within the budget.Usually transient; the SDK already retried. Surface a reload hint.
REQUEST_TIMEOUTNo response in 30 s.Offer retry; don't auto-loop.
REQUEST_FAILED / backend codeThe backend handler threw.Show err.message; it's your own message.

Retry, don't loop

The SDK already self-heals the handshake (it re-announces on a backoff budget) and times out requests at 30 s. On top of that:

  • Retry idempotent reads (getNotes) a bounded number of times with backoff.
  • Don't blindly retry writes — a REQUEST_TIMEOUT doesn't tell you whether the write landed. Prefer making writes idempotent (e.g. a client-supplied id) so a retry is safe.
  • Never tight-loop on failure; you'll hammer the runtime and the user.

User-facing vs developer errors

Distinguish the two so users see help, not stack traces:

  • User errors (bad input, no permission) → a short, actionable message in the UI. These are expected; don't log them as failures.
  • System errors (timeout, protocol, unexpected) → a generic "something went wrong, try again" to the user, and the detail to your logs.

Unhandled stdout/stderr from your backend is captured by the runtime log collector, so a console.error with context is a fine breadcrumb — just keep secrets out of it (the runtime redacts type:"secret" settings from its own logs, but not your ad-hoc logging).