Error handling

Catch PlotError when your game can offer a useful retry or exit choice. Every Plot failure exposed as an error inherits from this class and has a stable lowercase code:

import Plot, { PlotError, PlotErrorCode } from '@plot.game/sdk'

try {
  await Plot.ready()
  startGame()
} catch (error) {
  if (!(error instanceof PlotError)) throw error

  console.error('Plot startup failed', error.code, error.message)

  if (
    error.code === PlotErrorCode.ConnectionFailed ||
    error.code === PlotErrorCode.ConnectionTimeout ||
    error.code === PlotErrorCode.ReadinessTimeout ||
    error.code === PlotErrorCode.CommunicationFailed
  ) {
    showStartupRetry()
  } else {
    showUnableToStart()
  }
}

Use the exported PlotErrorCode values for program logic. The human-readable message is for developer diagnosis and may change. Do not parse it or show it directly to players. An optional cause may help diagnostics, but only code is a stable failure field.

Error types

  • PlotConnectionError means the connection stage of Plot.ready() or its connection settings failed.
  • PlotOperationError means startup synchronization or a gameplay operation failed.
  • PlotNotReadyError means a readiness-dependent API—such as a state read or write, clock read, or channel send—was used before Plot installed the complete ready state. Its code is not-ready.

All three inherit from PlotError; most games need only the base class. instanceof remains reliable if a bundle contains more than one SDK copy.

Invalid arguments and values rejected by the SDK's local validation use JavaScript's TypeError. These are code defects to fix, not service problems to retry. Examples include an invalid player ID, unsafe time value, malformed parentOrigin, or channel payload containing a Date. If Plot rejects a value after submission, it reports a PlotError such as invalid-value instead.

How failures are reported

API shapeBehavior
Valid synchronous party, state, and time readsThrow PlotNotReadyError until the complete ready state is installed. Party and state reads throw PlotOperationError if Plot can no longer guarantee a consistent cached snapshot.
Synchronous APIs with documented input validationThrow TypeError for invalid input. With valid input, channel sends throw PlotNotReadyError until the complete ready state is installed and PlotOperationError if a reliable message cannot be safely retained or the participation, session, or runtime has ended; otherwise they return void with no receipt.
Plot.ready(), Plot.party.getJoinUrl(), synchronized-state writes, Plot.showSystemUi(), and promise-returning toolkit methodsReturn promises. Plot failures and invalid input reject rather than throw before returning. An early readiness-dependent operation rejects with PlotNotReadyError and does not start readiness. Audio preloading and unlocking work before readiness.
Updater functions, and schema functions used by a read or writeTheir original errors pass through unchanged.
Subscription listeners, including schema parsing performed for a notificationThe SDK reports the original exception to the browser and continues notifying other listeners.

Subscriptions can be registered before readiness. Optional Plot.connection diagnostics are also available during startup. Pre-registered initial snapshot callbacks run after the complete ready state is installed but just before ready() resolves, so reads, writes, sends, and time are available inside them.

Codes and recovery

These are the current PlotErrorCode values. Repeat an action only when it is still meaningful and its outcome is known.

CodeMeaningTypical response
connection-options-mismatchStartup calls supplied different connection settings.Fix the calls so they use the same settings.
connection-failedPlot could not establish the game connection.Offer a deliberate retry or exit.
connection-timeoutPlot did not connect before the startup deadline.Offer a deliberate retry or exit.
incompatible-sdkThis SDK and the current Plot environment cannot communicate safely.Republish with a supported SDK; exit this run.
not-in-plot-sessionThe URL was opened directly instead of through Plot.Use standalone fixtures or launch through Plot preview.
unsupported-browserA required browser API is missing.Show a fallback or unsupported-browser message.
user-activation-requiredThe action needs a current player interaction: system UI could not verify one, or the browser did not enable audio.Call showSystemUi() or Plot.toolkit.unlockAudio() directly from a click, tap, or keyboard handler. The SDK also attempts audio activation automatically on player interaction, but rejected playback still needs an explicit retry of the current shared cue after unlocking.
not-readyA readiness-dependent API was used before the complete ready state was installed.Await Plot.ready() first; other API calls never start readiness implicitly.
readiness-timeoutPlot did not make this game copy ready before the startup deadline.Offer a deliberate startup retry or exit.
invalid-synchronized-statePlot supplied a complete synchronized snapshot the SDK could not safely install.Stop, log the error, and offer reload or exit.
launch-deniedPlot refused this game launch for the party or player.Return to Plot; launch again only after the condition changes.
service-unavailableA required Plot service or optional interface was unavailable.Keep a usable fallback. During startup, offer reload or exit; do not loop on an optional system-UI request.
operation-timeoutAn asynchronous operation exceeded its deadline.For submitted writes or UI requests, treat the outcome as unknown. For an audio asset load, check the asset and connection before retrying.
operation-supersededThe installed world is current, but this older write's exact outcome is no longer available.Inspect synchronized state and action receipts. Do not blindly rerun the old updater.
operation-cancelledA local audio command was intentionally replaced, paused, stopped, or disposed before completion.Usually ignore the cancelled attempt; the newer control is authoritative for that object.
object-disposedA permanently released toolkit object was used again.Create a new object. Repeated audio stop() and dispose() remain safe.
audio-load-failedAn audio URL could not be fetched, including HTTP, network, or CORS failures.Check the asset URL and its access rules; retry when available.
audio-decode-failedAudio was invalid, unsupported, empty, or used an unsupported channel layout.Supply a valid browser-decodable mono or stereo clip.
audio-resource-limitAudio exceeded compressed-size or decoded-memory limits, or allocation failed.Use shorter/smaller clips or dispose unused audio objects.
audio-unavailableThe browser audio engine failed or became permanently unavailable.Keep the game usable without sound; offer a deliberate audio retry.
too-many-pending-writesThis running game copy has 32 Game State or Player Data writes pending.Await or combine earlier writes before trying another. The rejected write was not submitted.
communication-failedThe SDK could not exchange an operation with Plot.During startup, retry deliberately. After readiness, reconcile if reads remain available; a retained terminal error requires reload or exit.
revision-conflictState changed after the revision used for a write.Derive the change again from the newest snapshot.
invalid-valuePlot rejected a synchronized object value.Fix the value; do not retry it unchanged.
not-hostA non-host tried to write Game State.Write only when party.self.playerId === party.host.playerId.
not-connectedPlot could not accept a connection-dependent operation during an interruption.Keep cached state; wait for connection before a deliberate new write or UI request.
participation-endedThis player's current participation ended. It can prevent startup or end an active non-host game copy.Return to Plot; join again through the platform if appropriate.
session-endedThe current game session ended.Stop sending and return the player to Plot; Plot owns teardown and state cleanup.
internal-errorPlot encountered an unexpected problem or can no longer guarantee consistent cached state.Log it; reconcile state or offer reload/exit as appropriate.

Only an explicit Plot.ready() starts readiness; another SDK call never does so implicitly. Repeated calls are safe and share one attempt. A call that supplies parentOrigin must match any setting already in use; omitting it reuses the current setting. A failure with connection-failed, connection-timeout, readiness-timeout, or communication-failed clears that attempt, so a later explicit call on the same page can retry. Other startup failures are retained for that setup. Invalid options and connection-options-mismatch reject only the incorrect caller.

incompatible-sdk, launch-denied, participation-ended, and session-ended are terminal for the current launch. Plot adapts SDK releases that remain in its support window, so an incompatibility requires republishing with a supported version.

For Plot.showSystemUi(), operation-timeout means the panel may already have opened before its confirmation was lost. Do not automatically retry it. A service-unavailable result can also mean Plot suppressed a rapid repeat request; leave the current game UI usable instead of looping until the panel opens.

Plot.party.getJoinUrl() is read-only, so its operation-timeout has no hidden party mutation or UI side effect. You may retry deliberately once the runtime is connected and the URL is still needed. Each attempt asks Plot for the current authoritative URL.

The session ends for everyone when the host leaves or closes the party, or when the host ends the game. A game does not clear synchronized objects itself and must not expect that ended session's state in a later launch.

For synced audio, pause, stop, and a replacement cue may intentionally reject an earlier pending play/resume with operation-cancelled. That cancellation does not populate the object's error or emit onError. Initial operational failures both reject the method promise and notify onError; choose one path for player-facing messages. Later playback failures arrive through onError and the snapshot because a resolved promise cannot reject again.

Never blindly retry an unknown write

If gameState.set(), gameState.update(), playerData.setSelf(), or playerData.updateSelf() reports operation-timeout, the change may have been accepted even though its confirmation never reached the game. Repeating it could apply the intent twice.

Keep rendering the latest cached snapshot. Reconcile with a later ordered onChanged() event or a subscribe() callback with reason: 'resync', then decide whether the intended change is still needed. Plot does not expose a forced-resync method or turn an unknown outcome into success.

Cached state and connection status

Use the connected lifecycle callback to reconcile all state together. Plot.connection.recovery reports known missing reliable ranges through replayGap and identifies crash-restored state through kind. Checkpoint recovery can also lose later messages with no saved gap position; a null gap does not prove complete history after a crash. A replay gap does not itself make synchronized state unusable. Completed planned updates preserve accepted state and unexpired history; unexpected crashes can roll back to a periodic snapshot.

After readiness, party, Game State, and Player Data getters keep returning the latest cached snapshots during a temporary interruption; those values may be stale. Plot.time continues advancing from its latest synchronized anchor. Plot holds a bounded amount of recoverable work while restoring current synchronized state. In the same running page, the original readiness promise stays complete and Plot restores the connection without another ready() call. A temporarily unavailable service can leave that page disconnected and gameplay paused; Plot tries a fresh synchronization when the service reconnects. A refreshed iframe is a new page and calls ready() during normal startup.

If recovery fails irreparably, the original ready() promise stays resolved; calling it again does not reopen that SDK instance. Later valid party and state reads, state writes, and channel sends report the retained PlotOperationError. Reload the game page or exit rather than building a second readiness or reconnect loop. Connection diagnostics and the last synchronized clock anchor remain available for UI and logging.

Use Plot.connection only for an offline indicator or to delay a deliberate retry. disconnected means Plot cannot currently exchange synchronized realtime data. It may represent temporary recovery or a terminal failure, so use the failed operation's error to decide what recovery is possible. Ordinary game code does not open or manage the underlying realtime connection or a reconnect loop.

Plot.realtime.latest reports a separate optional transport. If its available field becomes false, replaceable samples are dropped while synchronized state and reliable events can stay connected. Pause cosmetic high-rate sends as needed; do not treat this status alone as a reason to stop shared gameplay.

Keep player copy friendly

Log the error name, code, message, and stack through your development logging. Do not include credentials, personal data, or secret game material.

Write player-facing text around the next useful choice—for example, “Plot lost contact with this game. Try again or return to the party”—instead of exposing a technical SDK message.