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.
| Class | Package | Thrown by |
|---|---|---|
SdkError | @uncorded/plugin-sdk | Backend SDK calls (bad args, missing setting, …). |
SdkProtocolError (extends SdkError) | @uncorded/plugin-sdk | A runtime reply that errored or didn't match the expected shape. |
PluginError | @uncorded/plugin-sdk-frontend | Frontend request/handshake failures. |
UploadError | @uncorded/plugin-sdk-frontend | sdk.files.upload failures. |
ProxyError | @uncorded/plugin-sdk-frontend | sdk.proxy.* bootstrap failures. |
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:
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 manifestpermissions. Declare it. (See Permissions.)UNKNOWN_ACTION— the frontend called an action you never registered withplugin.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:
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:
| Code | Meaning | What to do |
|---|---|---|
HANDSHAKE_TIMEOUT | The shell never issued a token within the budget. | Usually transient; the SDK already retried. Surface a reload hint. |
REQUEST_TIMEOUT | No response in 30 s. | Offer retry; don't auto-loop. |
REQUEST_FAILED / backend code | The 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_TIMEOUTdoesn'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).