Errors

Every failure the SDK surfaces is a PortalError — carrying a stable, machine-readable code you can safely branch on — or one of its named subclasses, for the failures a caller typically reacts to differently:

import {
  PortalError,
  InvalidApiKeyError,
  BlockedError,
  TokenExpiredError,
  NotMemberError,
  ChannelAtCapacityError,
  AnonymousNotAllowedError,
  NotYetSupportedError,
  DegradedError,
} from "@portalsdk/core";
Class code When
InvalidApiKeyError "invalid_api_key" A bad or unknown apiKey. Terminal — status goes to "blocked", no reconnect loop.
BlockedError "blocked" A gate or your own portal.config.ts middleware refused a send. Carries reason — end-user-visible copy.
TokenExpiredError "token_expired" The token was rejected as expired. A callback token is re-resolved once and retried; a still-failing retry, or a static string token, surfaces this.
NotMemberError "not_member" A membership channel with no row for this user — on connect, or on a to:-send.
ChannelAtCapacityError "channel_at_capacity" The channel refused admission at its hard cap.
AnonymousNotAllowedError "anonymous_not_allowed" The channel is configured anonymous: false and the token is anonymous.
NotYetSupportedError "not_yet_supported" A reserved surface was used (e.g. a where filter pushed to the server, attachments, non-text media). Typed but rejected loudly.
DegradedError "degraded" A send into an extension namespace whose extension is currently degraded; the channel itself keeps working.

Handling send failures

send() rejects with the relevant error:

import { Portal, BlockedError, PortalError } from "@portalsdk/core";

const portal = new Portal({ apiKey: "pk_your_publishable_key" });
const room = portal.channel("room-1");
room.acquire();

function showToUser(_message: string) {}

async function sendMessage() {
  try {
    await room.send({ content: { text: "hello" } });
  } catch (err) {
    if (err instanceof BlockedError) {
      showToUser(err.reason); // end-user-visible copy from your middleware or the platform
    } else if (err instanceof PortalError) {
      console.error(err.code, err.message);
    } else {
      throw err;
    }
  }
}

Connection-level refusals

Refusals that aren't tied to a single send() call — a bad API key, being banned, not being a member, hitting channel capacity — arrive on the status event instead, and move room.status to "blocked":

import { Portal } from "@portalsdk/core";

const portal = new Portal({ apiKey: "pk_your_publishable_key" });
const room = portal.channel("room-1");
room.acquire();

room.on("status", (status, error) => {
  if (status === "blocked" && error) {
    console.error("channel blocked:", error.code, error.message);
  }
});

"blocked" is terminal for that handle: there's no reconnect loop once it fires.