Report failures, retry only when it is safe, set deadlines and handle uncertain writes
Most bots need little code for reliability. The SDK reconnects on its own, keeps running when a handler fails and reports callback failures with their full cause. A handler only has to return the Result of its reply, and retry only when the SDK confirms that retrying is safe
Return the Reply's Result
The default API returns expected failures, such as a missing permission or a network error, in a Result instead of throwing. A handler that returns that Result hands a failure to the SDK
The SDK holds a rest Warn for an HTTP 401 or 403 rejection for up to one second. If the handler returns that same failure while the Warn is held and no on hook is configured for it, only the handler's failure record appears. See rejection logging
import { errors, runBot } from "@neontechspace/fluxerly"
await runBot({
token: process.env.FLUXER_BOT_TOKEN,
events: {
messageCreate: async ({ message, reply }) => {
if (message.content !== "!ping")
return undefined
const first = await reply("Pong!")
if (first.isOk() || !errors.isRetryable(first.error))
return first
await new Promise((resolve) => setTimeout(resolve, errors.retryAfterMs(first.error) ?? 1_000))
return reply("Pong!")
},
},
})
import { errors, runBot } from "@neontechspace/fluxerly"
await runBot({
token: process.env.FLUXER_BOT_TOKEN,
events: {
messageCreate: async ({ message, reply }) => {
if (message.content !== "!ping") return undefined
const first = await reply("Pong!")
if (first.isOk() || !errors.isRetryable(first.error)) return first
await new Promise((resolve) => setTimeout(resolve, errors.retryAfterMs(first.error) ?? 1_000))
return reply("Pong!")
},
},
})
A successful reply returns its Result and nothing else happens. A failed reply is sent again only when errors confirms that the first attempt did not post a message, so the bot never answers twice. Any remaining failure is returned, and the SDK reports it to on or logs it at Error with its message and suggested fix
Either return a failed Result or handle it in the handler, not both. A handler that logs a failure and then returns it reports the same failure twice
The bot itself follows the same rule. When a failure stops it, such as a rejected token, run logs that failure once and sets process to 1, so the top-level code needs no error handling
Report Failures That Have No Result
Some failures have no caller to return to: A throwing event handler or one that returns an Err, a failed command, a failing collector callback or filter, a message cache callback that throws or a subscription closed by a full queue with overflow: "stop". The SDK sends each of them to the client's on option with the original error and the IDs of the message involved, never its content. Without a hook it logs them at Error with the full message, stack and cause chain
import { createClient } from "@neontechspace/fluxerly"
export function createReportedClient(token, track) {
return createClient({ token, onError: (report) => track(report) })
}
import { createClient, type FailureReport } from "@neontechspace/fluxerly"
export function createReportedClient(token: string, track: (report: FailureReport) => Promise<void>) {
return createClient({ token, onError: (report) => track(report) })
}
The report method returns readable text for an error tracker or a chat alert. A failing hook is logged together with the original failure and never retried
How Reports Are Queued
Reports reach the hook one at a time in order. Up to 256 wait while the hook is busy.
Later reports are logged instead and counted in diagnostics(), so none disappear silently.
Shutdown does not wait for a hook that never finishes
Read an SDK Error
Every SDK error extends Fluxerly with a stable code, an optional fix hint, safe details and a standard cause:
- Readable text: The
describefunction prints the code, hint and cause chainError( error) - Safe retries: The
errorsfunction says whether repeating the same call is safe. For an Effect.is Retryable( error) Causeit is true only when theCausehas at least one reason and every reason is a retryable failure, so a defect, an interruption or an emptyCausegives false - Required wait: The
errorsfunction returns the wait in milliseconds that Fluxer or the SDK requires before a retry, or.retry After Ms( error) undefinedwhen the error carries none. For aCauseit returns the longest wait among its failures - Fluxer's reason: The
errorsfunction returns Fluxer's error code name, such as.api Code( error) "missing, orPermissions" undefined - Request reach: The
errorsfunction returns.outcome( error) "not,Dispatched" "rejected"or"unknown", orundefinedwhen the SDK cannot establish it, such as for cancellation. Only"notshows that a write was not sent, so treat any other value, includingDispatched" undefined, as a write that may have applied and check remote state before repeating it - Missing resource: The
errorsfunction is true for an HTTP.is Not Found( error, "member") 404, and with a resource name only when Fluxer named that resource as the missing one. A 404 does not say why, because Fluxer answersUNKNOWN_for a missing channel and also for message and reaction requests in a thread from a bot that is no longer in the community, while other requests from such a bot get a 403CHANNEL missingPermissions - Missing permissions: For
"missing,Permissions" errorlists the permissions Fluxer checks for the failed operation, such as.details .required Permissions ViewandChannel Send, when the SDK knows themMessages - Branching: The
errorsfunction calls the handler named by the error's.match( error, handlers) _tag
An unexpected SDK or cleanup failure is not an Err. It rejects the Promise with Sdk, whose reasons list every failure and whose cause is the first defect, or the first failure when there is no defect. In the example above, await run lets it end the process with Node.js's error output and a failing exit code. An application with its own top-level try/catch can alert, restart or print describe there instead
Set a Request Deadline and Allow Cancellation
Every request has a 30-second deadline by default. For REST calls, the client option rest changes that default, while gateway count and member-chunk requests keep their own 30-second default. Pass timeout to set the deadline of one call, and a signal to stop waiting when the caller cancels. Create an Abort for that task and pass its signal to this helper
export async function fetchUntilCancelled(client, message, signal) {
return await client.messages.fetch(message, {
signal,
timeoutMs: 5_000,
})
}
import type { Client, MessageReference } from "@neontechspace/fluxerly"
export async function fetchUntilCancelled(
client: Client,
message: MessageReference,
signal: AbortSignal,
) {
return await client.messages.fetch(message, {
signal,
timeoutMs: 5_000,
})
}
The Result contains the message or a typed error. Aborting the signal produces Cancelled, and the five-second deadline also stops the request. Both wait for SDK cleanup before the Result arrives. A provider can still complete work it already received, so keep the controller with the operation that owns the user request
Handlers in run receive a signal that is aborted when the bot stops. The bound reply already uses it
Send Once When the Outcome Is Uncertain
A lost response, timeout or cancellation after a message was sent can leave it posted. Such a failure is an uncertain write, and errors returns false for it. Do not send again just because the result is uncertain. Supply an application operation ID as the message nonce, and reconcile through an application workflow before deciding what to do next
export async function sendOnceAndDiagnose(client, channelId, content, operationId) {
const sent = await client.messages.send(channelId, { content, nonce: operationId }, { timeoutMs: 5_000 })
const diagnostics = client.diagnostics()
console.info("Fluxerly client capacity", {
state: diagnostics.state,
activeRequests: diagnostics.rest.activeRequests,
queuedRequests: diagnostics.rest.queuedRequests,
retainedMessages: diagnostics.caches.messages.retainedEntries,
})
return sent
}
import type { Client } from "@neontechspace/fluxerly"
export async function sendOnceAndDiagnose(
client: Client,
channelId: string,
content: string,
operationId: string,
) {
const sent = await client.messages.send(
channelId,
{ content, nonce: operationId },
{ timeoutMs: 5_000 },
)
const diagnostics = client.diagnostics()
console.info("Fluxerly client capacity", {
state: diagnostics.state,
activeRequests: diagnostics.rest.activeRequests,
queuedRequests: diagnostics.rest.queuedRequests,
retainedMessages: diagnostics.caches.messages.retainedEntries,
})
return sent
}
The helper makes one send call, returns its Result, and logs only local state, request counts and cache counts from diagnostics. A nonce has best-effort duplicate suppression for a limited window, not durable idempotency or exactly-once delivery
Start, Recover and Stop the Connection in One Function
Without run, use client when one signal should control startup, recovery and shutdown. Pass a disconnected client with its handlers already registered and an Abort controlled by the application
export async function runUntilStopped(client, signal) {
return await client.run({ signal })
}
import type { Client } from "@neontechspace/fluxerly"
export async function runUntilStopped(client: Client, signal: AbortSignal) {
return await client.run({ signal })
}
Once run() starts, it waits through connection startup and recovery. It finishes after a normal stop, permanent failure or cancellation, once SDK cleanup is done
The run method requires a disconnected client and closes it permanently once the accepted run ends. Alternatively, use connect for startup, wait for later terminal failure and shutdown for final cleanup. Aborting wait alone does not stop the client
Limits and Retry Rules
The SDK applies these rules automatically. They need no configuration, but explain behavior seen in logs and diagnostics
Automatic Request Retries
A GET request that fails with a network error or HTTP 500, 502, 503 or 504 is retried at most twice. A connection lost while the response body arrives counts as a network error.
A write is sent again only after Fluxer confirms a rate limit with HTTP 429, so a write with an unknown outcome is never repeated automatically.
Each retry is counted in diagnostics() and logged at Debug
Rate Limits
The SDK waits for rate limits on its own and logs waits of one second or more at Warn.
Waiting counts toward each operation's deadline. A wait that would pass the deadline fails at once with ratelimit, including for an operation queued behind an earlier pause, and the error's retry names the remaining wait.
A global HTTP 429 pauses unrelated API routes on the same client, including when a valid global header accompanies an unreadable error body. Cancelling one queued operation does not clear the shared pause.
Server-provided bucket identifiers refine request grouping automatically, so bot code does not configure rate-limit groups. Matching limits share a wait, while independently limited resources stay separate. The first requests can still receive a 429 before the grouping is learned.
The client contract explains scope metadata and retry boundaries
Request Queue and Capacity
By default, a client runs up to 4 API requests at a time per local shard, capped at 64 for the client, and lets 256 requests wait. An explicit rest stays fixed instead of scaling with the shard count. A request that finds the queue full fails at once with reason busy and was never sent, so it is safe to retry later.
The rest client option changes these limits, as described in configuration
Rejected Gateway Messages
Gateway receive protection is automatic. A message beyond the fixed receive ceiling, or invalid UTF-8, stops the connection rather than repeatedly reconnecting to the same rejected data.
A known event that fails validation is skipped instead: The SDK logs gateway at Warn with the event type and failing field, clears cache entries it could have changed and keeps the session running.
Set gateway: { on to end the client instead.
The client contract documents the ceiling and failure codes. Subscription queue limits apply separately and do not bound JSON parsing memory
Reconnects
After a lost connection the SDK resumes the session when Fluxer allows it and otherwise starts a new one, waiting a growing random delay between attempts.
The default recovery keeps retrying transient failures. A permanent failure, such as a rejected token, ends the run with a connection error.
The connection option changes the delays, as listed in Connection