Error
One error type across the library — carrying a code, a module, and structured details alongside the message. Import from the root or from koyojs/Error.
// Root import
import { KoyoError, isKoyoError, assert } from "koyojs";
// Per-module import
import { KoyoError, KoyoTypeError, createError } from "koyojs/Error";The String, Number, Array, Object, and Math modules still throw native TypeError and RangeError. Changing that would break anyone catching them today, so the Koyo error types are what State and DOM throw, and what you build on for your own code.
The base class for every error KoyoJS throws, and the one to extend for your own. A real `Error` with a stack, plus a `code` and `module` so callers can branch on the failure without string-matching the message, a `details` bag for structured context, a `cause` for the lower-level error being wrapped, and `toJSON()` for structured logging.
| Parameter | Type | Description |
|---|---|---|
message | string | Human-readable description. |
options.code | string | Stable, machine-readable identifier. Defaults to `"KOYO_ERROR"`. |
options.module | string | Originating module, e.g. `"State"`. Defaults to `"Koyo"`. |
options.details | Record<string, unknown> | Structured context. Kept out of the message so logs stay greppable. |
options.cause | unknown | The lower-level error this one wraps. |
| returns | KoyoError | — |
import { KoyoError } from "koyojs/Error";
throw new KoyoError("Config file could not be read", {
code: "CONFIG_UNREADABLE",
module: "Config",
details: { path: "/etc/app.toml" },
});
// Branch on the code, not the message text
try {
loadConfig();
} catch (err) {
if (err instanceof KoyoError && err.code === "CONFIG_UNREADABLE") {
useDefaults();
} else {
throw err;
}
}
// Structured logging — details stay a separate field
try {
loadConfig();
} catch (err) {
if (err instanceof KoyoError) {
logger.error(err.toJSON());
// { name, code, module, message, details }
}
}
// Wrapping a lower-level failure without losing it
try {
await db.connect();
} catch (cause) {
throw new KoyoError("Database unavailable", {
code: "DB_DOWN",
module: "Storage",
cause,
});
}A value was the wrong type or shape. Same constructor as `KoyoError` — the subclass exists so callers can catch the category rather than inspecting a code.
| Parameter | Type | Description |
|---|---|---|
message | string | Human-readable description. |
options | KoyoErrorOptions | Same options as `KoyoError`. |
| returns | KoyoTypeError | — |
import { KoyoTypeError } from "koyojs/Error";
function parsePort(value: unknown): number {
if (typeof value !== "number") {
throw new KoyoTypeError("port must be a number", {
code: "BAD_PORT",
details: { received: typeof value },
});
}
return value;
}A value was the right type but outside the range the function accepts. Use it to distinguish "you passed a number, but -1 is not allowed" from "you passed a string".
| Parameter | Type | Description |
|---|---|---|
message | string | Human-readable description. |
options | KoyoErrorOptions | Same options as `KoyoError`. |
| returns | KoyoRangeError | — |
import { KoyoRangeError } from "koyojs/Error";
function setVolume(level: number) {
if (level < 0 || level > 100) {
throw new KoyoRangeError("volume must be between 0 and 100", {
code: "VOLUME_OUT_OF_RANGE",
details: { level },
});
}
}Thrown by `koyojs/State` — a misused hook, a missing reactive scope, or a runaway update loop. Its `module` field defaults to `"State"`, so you do not have to set it when throwing from your own reactive code.
| Parameter | Type | Description |
|---|---|---|
message | string | Human-readable description. |
options | KoyoErrorOptions | Same options as `KoyoError`. `module` defaults to `"State"`. |
| returns | KoyoStateError | — |
import { KoyoStateError } from "koyojs/Error";
import { useState, onCleanup } from "koyojs/State";
// Thrown by the library when the rules of hooks are broken
useState(0); // outside component() → KoyoStateError
onCleanup(() => {}); // outside a reactive scope → KoyoStateError
// Also thrown when an effect writes a signal it reads,
// after 1000 flush passes: "Maximum update depth exceeded"
try {
renderApp();
} catch (err) {
if (err instanceof KoyoStateError) {
console.error("Reactive bug:", err.toJSON());
}
}Thrown by `koyojs/DOM` — no document available (server-side render), an invalid selector, or an unusable target. Its `module` field defaults to `"DOM"`.
| Parameter | Type | Description |
|---|---|---|
message | string | Human-readable description. |
options | KoyoErrorOptions | Same options as `KoyoError`. `module` defaults to `"DOM"`. |
| returns | KoyoDOMError | — |
import { KoyoDOMError } from "koyojs/Error";
import { $ } from "koyojs/DOM";
// Running $ where there is no document (SSR, worker) throws
try {
$(".card").addClass("active");
} catch (err) {
if (err instanceof KoyoDOMError) {
// Skip DOM work on the server rather than crashing the render
return;
}
throw err;
}Builds a custom error class with `code`, `module`, and `details` defaults baked in, still inside the `KoyoError` hierarchy — so `isKoyoError` and `instanceof KoyoError` both still recognise it. Options passed at throw time win over the defaults.
| Parameter | Type | Description |
|---|---|---|
name | string | Class name, used as the error's `name` and in stack traces. |
defaults | KoyoErrorOptions | Options baked into every instance. Per-throw options override these. |
| returns | KoyoErrorClass | — |
import { createError, isKoyoError } from "koyojs/Error";
const PaymentError = createError("PaymentError", {
code: "PAYMENT",
module: "Billing",
});
throw new PaymentError("Card declined", {
details: { last4: "4242" },
});
// name: "PaymentError", code: "PAYMENT", module: "Billing"
// Per-throw options win over the baked-in defaults
throw new PaymentError("Gateway timed out", { code: "PAYMENT_TIMEOUT" });
// code: "PAYMENT_TIMEOUT"
// Still part of the hierarchy
try {
charge();
} catch (err) {
isKoyoError(err); // true
err instanceof PaymentError; // true
}
// One class per failure domain keeps catch blocks readable
const AuthError = createError("AuthError", { code: "AUTH", module: "Auth" });
const QuotaError = createError("QuotaError", { code: "QUOTA", module: "Limits" });Type guard for any error produced by KoyoJS or by `createError`. Checks a registry-symbol brand rather than the prototype chain, so it stays correct across realms (iframes, workers) and across duplicate installs of the library in one dependency tree — cases where a plain `instanceof` silently returns false.
| Parameter | Type | Description |
|---|---|---|
value | unknown | The caught value to test. |
| returns | value is KoyoError | — |
import { isKoyoError } from "koyojs/Error";
try {
await doWork();
} catch (err) {
if (isKoyoError(err)) {
// Narrowed — code, module, details, toJSON() all available
logger.error({ code: err.code, module: err.module, ...err.details });
} else {
logger.error({ message: String(err) });
}
}
// Correct across realms, where instanceof is not
const iframeError = getErrorFromIframe();
iframeError instanceof KoyoError; // false — different realm, different class
isKoyoError(iframeError); // true
// Same story with two copies of koyojs in one dependency tree
isKoyoError(errorFromNestedCopy); // trueThrows when `condition` is falsy, and narrows the condition for the code that follows — after `assert(x !== null, ...)`, TypeScript knows `x` is non-null. `options.error` picks which class to throw; everything else matches `KoyoError`'s options.
| Parameter | Type | Description |
|---|---|---|
condition | unknown | Throws when falsy. Narrowed for subsequent code. |
message | string | Message for the thrown error. |
options.error | KoyoErrorClass | Error class to throw. Defaults to `KoyoError`. |
options | KoyoErrorOptions | Also accepts code, module, details, cause. |
| returns | void (narrows condition) | — |
import { assert, KoyoRangeError, KoyoTypeError } from "koyojs/Error";
function head<T>(items: T[]): T {
assert(items.length > 0, "items must not be empty", {
error: KoyoRangeError,
});
return items[0]; // no non-null assertion needed
}
// Narrowing removes the need for a manual type guard
function greet(name: string | null) {
assert(name !== null, "name is required", { error: KoyoTypeError });
return name.toUpperCase(); // name is string here
}
// Carries the same structured context as a hand-thrown error
assert(user.role === "admin", "admin required", {
code: "FORBIDDEN",
module: "Auth",
details: { role: user.role },
});