Errors and Disconnects
How Errors Are Reported
Errors reach your code through three channels.
Failures of an operation you started arrive as that operation's failed outcome; this is where expected failures such as a full room land.
In C++ that is the Err state of the operation's Result, over the C API the error code in the mode of its result event, and in JavaScript a rejection with a RealtimeError.
Errors that occur outside any pending operation are reported through the error callback: OnError(ClientError, message) in C++ and the 'error' event in JavaScript.
This includes the server or a plugin rejecting an operation that no pending operation tracks.
Loss of the connection itself is reported through the disconnect callback with a disconnect cause: OnDisconnected(DisconnectCause) in C++ and the 'disconnected' event in JavaScript.
Every failure is described by an error value.
Its code is the machine-readable value your logic branches on; its message is a human-readable UTF-8 string meant for logs and diagnostics, not for parsing.
The code is one of the operation's own error codes, which list only the codes that operation can report: joining a room, below, reports the join codes.
In C++ these are the Code and Message of an Error, and the code's type is the operation's own error enum, JoinRoomError for JoinRoom().
In JavaScript they are the code, codeValue and message of a RealtimeError: code is the name as a string, one of the JoinRoomErrorCode codes for joinRoom(), and codeValue the numeric value.
Only failed operations reject with a RealtimeError. Misusing the API, such as calling a disposed client, throws a plain Error, TypeError or RangeError synchronously.
The standard handling shape checks the outcome, branches on the code and logs the message:
C++
Result<Room, JoinRoomError> joined = co_await client.JoinRoom(PHOTON_STR("battle-01"));
if (joined.IsErr())
{
switch (joined.GetErrorCode())
{
case JoinRoomError::RoomNotFound:
// fall back to creating the room
break;
case JoinRoomError::RoomFull:
// tell the player and return to matchmaking
break;
default:
std::printf("join failed: %s\n",
reinterpret_cast<const char*>(joined.GetError().Message.c_str()));
break;
}
}
C
realtime_join_room(client, "battle-01", 0);
/* In the polled batch: */
if (e->type == RealtimeEventType_JoinRoomResult && e->mode != 0)
{
switch (e->mode)
{
case RealtimeJoinRoomError_RoomNotFound:
/* fall back to creating the room */
break;
case RealtimeJoinRoomError_RoomFull:
/* tell the player and return to matchmaking */
break;
default:
printf("join failed: %.*s\n", (int)e->blobLength,
(e->blobOffset >= 0) ? (const char*)(blob + e->blobOffset) : "");
break;
}
}
/* mode is the numeric ErrorCode and the blob is the message, so the same
three-step shape applies: check mode, branch on it, print the blob.
Errors that occur outside a pending operation arrive as the Error (101)
event with the code in origin and the message in the blob, and warnings
as Warning (113) with the WarningCode in origin and the message in the
blob. */
JavaScript
import { isRealtimeError } from '@photonengine/realtime-core';
try {
await client.joinRoom('battle-01');
} catch (error) {
if (!isRealtimeError(error)) {
throw error; // a programming error, not a failed operation
}
switch (error.code) {
case 'roomNotFound':
await client.createRoom('battle-01'); // fall back to creating the room
break;
case 'roomFull':
// tell the player and return to matchmaking
break;
default:
console.log(`join failed: ${error.message}`);
break;
}
}
Errors that occur outside a pending operation arrive as the 'error' event, whose payload is a RealtimeError with one of the ClientErrorCode codes.
Error Codes
Each operation reports its own set of error codes, so the codes a call can fail with are known up front: the connect codes for connecting and reconnecting, the join codes for the four ways of joining a room, the client codes for the error callback, and so on. Every set is a subset of the full list of error codes, and a code shares its name and number across every set it appears in. A server code an operation cannot report arrives as that operation's unknown code, with the server's code in the message.
| Codes | C++ | C | JavaScript |
|---|---|---|---|
| All codes | ErrorCode |
RealtimeErrorCode |
ErrorCode, the string in RealtimeError.code |
| One operation's codes | The operation's error enum, such as ConnectError or JoinRoomError, which widens implicitly to ErrorCode |
The operation's error enum, such as RealtimeConnectError or RealtimeJoinRoomError, reported in mode |
A string union type, such as ConnectErrorCode or JoinRoomErrorCode |
The codes of each operation and the numeric value of every code, which JavaScript also reports as codeValue, are listed in the enums reference.
The following table groups all codes by area:
| Area | C++ | JavaScript |
|---|---|---|
| General | Ok, Unknown |
'ok', 'unknown' |
| Connection | ConnectionFailed, Disconnected, ServerFull, NotConnected, AlreadyConnected |
'connectionFailed', 'disconnected', 'serverFull', 'notConnected', 'alreadyConnected' |
| Authentication | InvalidAuthentication, CustomAuthenticationFailed, AuthenticationTicketExpired, MaxCCUReached, InvalidRegion, ClientVersionTooOld |
'invalidAuthentication', 'customAuthenticationFailed', 'authenticationTicketExpired', 'maxCcuReached', 'invalidRegion', 'clientVersionTooOld' |
| Room and matchmaking | RoomFull, RoomClosed, RoomNotFound, RoomAlreadyExists, NoMatchFound, AlreadyJoined, SlotError, NotInRoom, AlreadyInRoom, RejoinNotFound, UserExcluded |
'roomFull', 'roomClosed', 'roomNotFound', 'roomAlreadyExists', 'noMatchFound', 'alreadyJoined', 'slotError', 'notInRoom', 'alreadyInRoom', 'rejoinNotFound', 'userExcluded' |
| Operation | OperationDenied, OperationInvalid, OperationLimitReached, OperationPending, RequestFailed, InvalidArgument |
'operationDenied', 'operationInvalid', 'operationLimitReached', 'operationPending', 'requestFailed', 'invalidArgument' |
| Server | InternalServerError, PluginError |
'internalServerError', 'pluginError' |
| Events | EventSizeMismatch, EventEncodeFailed, EventDecodeFailed, EventPayloadTypeMismatch |
'eventSizeMismatch', 'eventEncodeFailed', 'eventDecodeFailed', 'eventPayloadTypeMismatch' |
The C API spells each code with the enum's name as prefix, such as RealtimeErrorCode_RoomFull or RealtimeJoinRoomError_RoomFull.
In JavaScript, a numeric code the package does not know reads as 'unknown', and codeValue still holds the number.
| C++ | JavaScript | Typical Trigger | Sensible Reaction |
|---|---|---|---|
RoomFull |
'roomFull' |
Joining a room whose maximum player count is reached. | Pick another room or fall back to random matchmaking. |
RoomNotFound |
'roomNotFound' |
Joining a named room that does not exist or has closed. | Create the room, or join or create it in one operation up front. |
RoomAlreadyExists |
'roomAlreadyExists' |
Creating a room whose name is taken. | Join it instead, or join or create it in one operation up front. |
NoMatchFound |
'noMatchFound' |
Random matchmaking found no suitable room. | Create a room, or relax the matchmaking filter and retry. |
NotConnected, AlreadyConnected, AlreadyInRoom, NotInRoom |
'notConnected', 'alreadyConnected', 'alreadyInRoom', 'notInRoom' |
The operation is not valid in the current client state. | Fix the call order; do not retry blindly. |
OperationPending |
'operationPending' |
The same operation is already in flight. | Await the pending call instead of issuing another. |
MaxCCUReached |
'maxCcuReached' |
The application's concurrent-user limit is exhausted. | Increase the max CCU by upgrading your plan in the Photon Dashboard. |
OperationLimitReached |
'operationLimitReached' |
Operations were issued faster than the server allows. | Throttle the offending call site and retry after a delay. |
PluginError |
'pluginError' |
A server-side plugin rejected or failed the operation. | Log the message; the fix is usually server-side. |
EventSizeMismatch |
'eventSizeMismatch' |
In C++, an event arrived whose payload size does not match sizeof(T) of a SubscribeEvent<T>() subscription. |
Version the event code, or receive it raw and branch on the size. |
EventPayloadTypeMismatch |
'eventPayloadTypeMismatch' |
In C++, a byte-span event subscription received a structured payload. | Subscribe with const RealtimeValue& for that code. |
Handling Disconnects
Distinguish deliberate from unexpected disconnects.
A disconnect you request completes its own operation, so its completion is the natural place to react.
Unexpected drops, such as timeouts and server-side closes, fire the disconnect callback with the cause.
The client retains the last cause for post-mortem checks: GetDisconnectCause() in C++, realtime_disconnect_cause() in C and disconnectCause in JavaScript.
The disconnect callback also fires for a disconnect you request, with the cause None ('none' in JavaScript).
| C++ | JavaScript | Recommended Reaction |
|---|---|---|
TimeoutDisconnect, Exception, ExceptionOnConnect |
'timeoutDisconnect', 'exception', 'exceptionOnConnect' |
Transient network trouble. Attempt a reconnect, then a fresh connect. |
DisconnectByServer, DisconnectByServerLogic |
'disconnectByServer', 'disconnectByServerLogic' |
The server closed the connection. Inform the player and reconnect with backoff. |
AuthenticationTicketExpired |
'authenticationTicketExpired' |
The authentication ticket aged out. Re-authenticate and connect again. |
InvalidAuthentication, CustomAuthenticationFailed, InvalidRegion, DashboardVersionInvalid |
'invalidAuthentication', 'customAuthenticationFailed', 'invalidRegion', 'dashboardVersionInvalid' |
Credentials or configuration are wrong. Fix the app id, auth values or dashboard settings; retrying unchanged fails again. |
ClientVersionTooOld, ClientVersionInvalid |
'clientVersionTooOld', 'clientVersionInvalid' |
The client build is no longer accepted. Prompt the player to update. |
MaxCCUReached, DisconnectByServerUserLimit, DisconnectByOperationLimit |
'maxCcuReached', 'disconnectByServerUserLimit', 'disconnectByOperationLimit' |
A server-enforced limit was hit. Back off before retrying. |
OperationNotAllowedInCurrentState |
'operationNotAllowedInCurrentState' |
A local call-ordering bug triggered the disconnect. Fix the code. |
None |
'none' |
No disconnect has been recorded, or the disconnect was requested. |
The following handler separates transient timeouts from server-initiated closes:
C++
RealtimeCore::Common::ScopedSubscription onDisconnected = client.OnDisconnected.Subscribe(
[](DisconnectCause cause) {
switch (cause)
{
case DisconnectCause::TimeoutDisconnect:
case DisconnectCause::Exception:
// transient: try to restore the session
break;
case DisconnectCause::DisconnectByServer:
case DisconnectCause::DisconnectByServerLogic:
// server-initiated: inform the player before retrying
break;
default:
// configuration or version problems: do not auto-retry
break;
}
});
C
/* Disconnected (100) carries the DisconnectCause in `origin`. */
if (e->type == RealtimeEventType_Disconnected)
{
switch (e->origin)
{
case 5: /* TimeoutDisconnect */
case 6: /* Exception */
/* transient: try to restore the session */
break;
case 3: /* DisconnectByServer */
case 4: /* DisconnectByServerLogic */
/* server-initiated: inform the player before retrying */
break;
default:
/* configuration or version problems: do not auto-retry */
break;
}
}
/* The numeric values come from the DisconnectCause table, and
realtime_disconnect_cause returns the last one after the fact. A
Disconnect that you issued yourself still reports its own
DisconnectResult, so the two channels stay distinguishable. */
JavaScript
client.on('disconnected', ({ cause }) => {
switch (cause) {
case 'none':
break; // your own disconnect()
case 'timeoutDisconnect':
case 'exception':
console.log('transient, restore the session:', cause);
break;
case 'disconnectByServer':
case 'disconnectByServerLogic':
console.log('closed by the server, inform the player before retrying:', cause);
break;
default:
console.log('configuration or version problem, do not auto-retry:', cause);
break;
}
});
Warnings
The warning callback reports non-fatal conditions the SDK noticed.
Like an error, a warning pairs a machine-readable warning code with a human-readable UTF-8 message that also names the underlying Photon status code, meant for logs rather than parsing.
In C++ the callback is OnWarning(WarningCode, message), over the C API the Warning event with the RealtimeWarningCode in origin and the message in the blob, and in JavaScript the 'warning' event with a { code, message } payload whose code is a WarningCode string such as 'outgoingReliableQueueFilling'.
The codes all report one of the client's internal queues filling up, usually because the client is serviced too rarely or because the server stops acknowledging reliable commands; the complete list lives in the enums reference.
In a browser, the timers of a background tab are throttled, which slows the automatic pump down; see Client and Service Loop.
Treat warnings as log-worthy signals: record them with enough context to correlate against later problems, but do not build control flow on them.
Recovery Patterns
The primary recovery tool is reconnecting: Reconnect() in C++, realtime_reconnect() in C and reconnect() in JavaScript.
Called from the disconnected state, it re-establishes the previous session and rejoins the room the client was last in, provided the room was created with a player time-to-live (PlayerTtlMs in C++, playerTtlMs in JavaScript) large enough that the player's slot is still reserved.
See Connection and Regions for the reconnect semantics and rejoin preconditions.
When a reconnect is not enough, escalate step by step.
Try a reconnect first; if it fails, make a fresh connect followed by joining the room as a rejoin (Rejoin = true in C++, { rejoin: true } in JavaScript); if the room or the player slot is gone, re-enter matchmaking from scratch.
The following recovery flow falls back from a reconnect to a full connect-and-rejoin:
C++
Task<Result<void>> ConnectAndRejoin(RealtimeClient& client, RealtimeCore::Common::StringType roomName)
{
Result<void, ConnectError> connected = co_await client.Connect();
if (connected.IsErr())
{
co_return connected;
}
JoinRoomOptions options;
options.Rejoin = true;
Result<Room, JoinRoomError> joined = co_await client.JoinRoom(roomName, options);
co_return joined.IsOk() ? Result<void>::Ok() : Result<void>::Err(joined.GetError());
}
Task<Result<void>> RecoverSession(RealtimeClient& client, RealtimeCore::Common::StringType roomName)
{
return client.Reconnect()
.OrElse([&client, roomName](const auto&) { return ConnectAndRejoin(client, roomName); });
}
C
/* The escalation is explicit: reconnect, then connect-and-rejoin, then
matchmaking from scratch. */
typedef enum { Recover_Reconnecting, Recover_Connecting, Recover_Rejoining, Recover_Failed } RecoverStage;
static RecoverStage stage;
static const char* roomName;
static void RecoverStart(RealtimeHandle client)
{
stage = Recover_Reconnecting;
realtime_reconnect(client);
}
static void RecoverOnEvent(RealtimeHandle client, const RealtimeEvent* e)
{
if (e->type == RealtimeEventType_ReconnectResult && stage == Recover_Reconnecting)
{
if (e->mode == 0)
{
stage = Recover_Rejoining; /* reconnect also rejoined the room */
return;
}
stage = Recover_Connecting;
realtime_connect(client, NULL, NULL, 255, NULL, NULL);
}
else if (e->type == RealtimeEventType_ConnectResult && stage == Recover_Connecting)
{
if (e->mode != 0)
{
stage = Recover_Failed;
return;
}
stage = Recover_Rejoining;
realtime_join_room(client, roomName, 1); /* rejoin */
}
else if (e->type == RealtimeEventType_JoinRoomResult && stage == Recover_Rejoining && e->mode != 0)
{
/* The room or the player slot is gone: re-enter matchmaking. */
stage = Recover_Failed;
}
}
/* Without OrElse the fallback chain becomes a stage variable, and the
stage rather than the event decides which step a *Result belongs to.
realtime_reconnect has the same preconditions as the C++ call: it is
only valid from the Disconnected state, and restoring the player slot
still needs a non-zero PlayerTtlMs on the room. */
JavaScript
const lastRoom = 'battle-01';
async function recoverSession() {
try {
await client.reconnect(); // also rejoins the last room while the player slot is reserved
} catch {
await client.connect();
await client.joinRoom(lastRoom, { rejoin: true });
}
}
A try block around the awaited reconnect takes the place of OrElse.
For failures that are expected within a flow, such as a full room during matchmaking or a name collision when creating, prefer recovering in place inside the operation chain over out-of-band retry loops.
In C++ that is OrElse, in JavaScript catch(); see Asynchronous Operations for chaining.
Last updated on
Back to top