Documentation / Error

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.

KoyoErrorclass KoyoError extends Error

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.

ParameterTypeDescription
messagestringHuman-readable description.
options.codestringStable, machine-readable identifier. Defaults to `"KOYO_ERROR"`.
options.modulestringOriginating module, e.g. `"State"`. Defaults to `"Koyo"`.
options.detailsRecord<string, unknown>Structured context. Kept out of the message so logs stay greppable.
options.causeunknownThe lower-level error this one wraps.
returnsKoyoError
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,
  });
}

KoyoTypeErrorclass KoyoTypeError extends KoyoError

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.

ParameterTypeDescription
messagestringHuman-readable description.
optionsKoyoErrorOptionsSame options as `KoyoError`.
returnsKoyoTypeError
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;
}

KoyoRangeErrorclass KoyoRangeError extends KoyoError

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".

ParameterTypeDescription
messagestringHuman-readable description.
optionsKoyoErrorOptionsSame options as `KoyoError`.
returnsKoyoRangeError
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 },
    });
  }
}

KoyoStateErrorclass KoyoStateError extends KoyoError

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.

ParameterTypeDescription
messagestringHuman-readable description.
optionsKoyoErrorOptionsSame options as `KoyoError`. `module` defaults to `"State"`.
returnsKoyoStateError
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());
  }
}

KoyoDOMErrorclass KoyoDOMError extends KoyoError

Thrown by `koyojs/DOM` — no document available (server-side render), an invalid selector, or an unusable target. Its `module` field defaults to `"DOM"`.

ParameterTypeDescription
messagestringHuman-readable description.
optionsKoyoErrorOptionsSame options as `KoyoError`. `module` defaults to `"DOM"`.
returnsKoyoDOMError
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;
}

createErrorcreateError(name: string, defaults?: KoyoErrorOptions): KoyoErrorClass

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.

ParameterTypeDescription
namestringClass name, used as the error's `name` and in stack traces.
defaultsKoyoErrorOptionsOptions baked into every instance. Per-throw options override these.
returnsKoyoErrorClass
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" });

isKoyoErrorisKoyoError(value: unknown): value is KoyoError

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.

ParameterTypeDescription
valueunknownThe caught value to test.
returnsvalue 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);  // true

assertassert(condition: unknown, message: string, options?: AssertOptions): asserts condition

Throws 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.

ParameterTypeDescription
conditionunknownThrows when falsy. Narrowed for subsequent code.
messagestringMessage for the thrown error.
options.errorKoyoErrorClassError class to throw. Defaults to `KoyoError`.
optionsKoyoErrorOptionsAlso accepts code, module, details, cause.
returnsvoid (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 },
});