Error Handling
The SDK surfaces separate HTTP, cancellation, validation, socket decode, and transport failure channels. Catch the specific channel you expect.
EngineHttpError
Thrown by any HTTP method on SimulatorClient when the server responds with a non-2xx status. The body is parsed against the api-error-response schema before the error is thrown.
import { EngineHttpError, createSimulatorClient } from '@my-swu/simulator-client'
const simulatorClient = createSimulatorClient({ baseUrl: 'http://127.0.0.1:4000' })
try {
// Missing matches demonstrate the non-2xx HTTP error path.
await simulatorClient.matches.get('missing-match')
}
catch (error) {
if (error instanceof EngineHttpError) {
// Prefer structured fields over parsing the message string.
console.warn(error.status, error.code, error.method, error.url, error.message)
}
}Fields:
status— HTTP status code.code— engine error code (for examplematch_not_found).method— HTTP method used for the request.url— fully resolved request URL.payload— parsed JSON body when available.responseText— raw body text when available.message— human-readable message from the engine.
Common deck and format codes:
invalid_selection— deck shape or gameplay choice failed validation. The message names the exact rule, such as sideboard size or copy count.unsupported_format— requested match format or player count is schema-valid but not supported by the current runtime.
EngineRequestAbortedError
Thrown by any HTTP method on SimulatorClient when the request is cancelled before the server answers — either because the caller's AbortSignal fired or because a timeout elapsed. Nothing was validated and no status was received, so this is its own channel next to EngineHttpError.
import { EngineRequestAbortedError, createSimulatorClient } from '@my-swu/simulator-client'
const simulatorClient = createSimulatorClient({
baseUrl: 'http://127.0.0.1:4000',
// Client-level default; any call can override it with its own `timeoutMs`.
timeoutMs: 10_000,
})
const unmountController = new AbortController()
try {
await simulatorClient.matches.get('match_123', { signal: unmountController.signal })
}
catch (error) {
if (error instanceof EngineRequestAbortedError) {
// A caller abort is expected teardown; a timeout is worth reporting.
if (error.kind === 'timeout') {
console.warn('simulator timed out after', error.timeoutMs, error.url)
}
}
}Fields:
kind—'aborted'when the caller's signal fired,'timeout'when the per-call or client-level timeout elapsed first.method— HTTP method used for the cancelled request.url— fully resolved request URL.timeoutMs— timeout that applied to the request, when one did.
See Cancellation and timeouts for how the per-call and client-level controls combine.
SdkValidationError
Thrown by parseSchema when a payload does not match the generated schema.
A client only raises it when it was created with a validator — boundary validation is off by default, so an SDK client built without validate: createSchemaValidator() never throws this error from its HTTP methods or its session socket. See Runtime validation. Calling parseSchema yourself always validates, whatever the client is configured with.
import { SdkValidationError } from '@my-swu/simulator-client'
try {
// With a validator installed, this fails before anything is sent.
matchSession.sendCommand(/* malformed */ {} as never)
}
catch (error) {
if (error instanceof SdkValidationError) {
// Details include schema paths that are useful for developer logs.
console.warn(error.schemaName, error.details)
}
}Fields:
schemaName— the schema the payload failed against.details— list ofpath + messagestrings from Ajv.
Socket-side decoding failures
Inbound socket payloads never throw. Bad JSON — or, when the client has a validator, a payload that does not match the socket's protocol schema — is emitted on the error event instead:
matchSession.on('error', errorPayload => {
if (errorPayload.code === 'sdk_invalid_server_message') {
// Transport-level decode failure.
}
else {
// Engine-side error payload.
}
})This keeps the socket listener resilient to transient garbage or schema drift without crashing the app. A message listener is the wrong place to throw from: an exception there escapes into the transport rather than reaching your code, so the SDK routes it to the session's own error channel instead.
Every socket answers the same way
Match sessions, lobby sessions, tournament-room sessions, and the public lobby-list subscription share one frame decoder, so the rule above is uniform across all four:
- Invalid JSON —
sdk_invalid_server_messageon the session'serrorevent, whatever the client is configured with. - A known frame type with a malformed payload —
sdk_invalid_server_messagewhen the client was built with a validator. Without one nothing is checked and the frame is trusted as sent. - An unknown frame
type— ignored, never an error. The server is allowed to grow message variants without breaking clients built against an older contract.
Each session exposes the same pair of surfaces: an error event and an accumulated errors list.
import { createSimulatorClient, createSchemaValidator } from '@my-swu/simulator-client'
const simulatorClient = createSimulatorClient({
baseUrl: 'http://127.0.0.1:4000',
// Inherited by every socket this client opens, not only match sessions.
validate: createSchemaValidator(),
})
const lobbySession = simulatorClient.lobbies.createSession({
lobbyId: 'lobby_123',
playerToken: 'player_token_123',
})
lobbySession.on('error', errorPayload => {
if (errorPayload.code === 'sdk_invalid_server_message') {
// Same channel, same code, same reason as the match socket above.
}
})
console.log(lobbySession.errors.length)lobbies.createSession(), lobbies.subscribeList(), and tournamentRooms.createSession() take the validator from the client that created them; there is no per-session validation option to set. A client left on the default validates nothing on any of them. See Runtime validation.
Transport failures
Network failures from fetch propagate as native TypeError (or similar, depending on the runtime). Cancelled requests are the exception: the SDK normalizes them into EngineRequestAbortedError so a caller never has to sniff runtime-specific AbortError shapes. WebSocket connect failures before the handshake completes reject the matchSession.connect() promise with a generic Error and are recorded in matchSession.errors.
