Understand the small set of Effect concepts needed to send a message and run a bot
Effect is optional. The default JavaScript and TypeScript API supports the same SDK capabilities with async functions and Results
Use the native Effect API when several bot operations must stop together, resources must close and failures need separate handling. Those operations can use the same services and cleanup scope as the rest of an Effect application
The examples introduce each Effect concept before combining them into a bot
Install the Matching Version
To start from a generated Effect project with a test, run the command from Generate the project with --template effect in the bot's folder, which may already hold the token's .env and a .gitignore.
First generate a project with the quick start and choose Effect, which installs the SDK, Effect, TypeScript 7 and the Node.js types for typechecking. Fluxerly is tested against Node.js 24.15 or newer. A project set up by hand needs "type": "module" in package.json and the TypeScript setup that the quick start's TypeScript section describes. Native examples use TypeScript
Fluxerly works with Effect 4. The installed SDK lists its accepted range under peer in node_. Effect 3 and Effect 4 prereleases are incompatible
These examples import Effect directly, so declare it as a direct dependency. For this documentation version, the command is:
npm install effectpnpm add effectbun add effectCheck which Effect version the application has installed:
npm list effectpnpm list effectbun why effectRecheck the required version when upgrading the SDK. The Effect v4 learning material matches this major version
Start with One Request
An Effect describes work but does not run it yet. Effect runs it from the application's top-level code
import { Effect } from "effect"
import { createClient } from "@neontechspace/fluxerly/effect"
export function sendHello(token: string, channelId: string) {
const program = Effect.scoped(
Effect.gen(function* () {
const client = yield* createClient({ token })
return yield* client.messages.send(channelId, "Hello from Effect")
}),
)
return Effect.runPromise(program)
}
Call send with a private bot token and a channel the bot can write to. It resolves to the sent message after the client has finished cleanup. Sending through REST does not need a gateway connection
Read the program in order:
Effectwrites sequential work using a generator function.gen - The
yield*expression runs an Effect and returns its successful value. An expected failure skips the remaining steps unless handled Effectsupplies a cleanup lifetime. When that lifetime ends, Fluxerly closes the client and waits for its owned cleanup.scoped Effectruns the program and returns a JavaScript Promise.run Promise
Each yield* provides the successful value directly, while failures remain part of the Effect's type. Running the same description twice repeats its requests, including writes
Add a Message Handler
Native handlers must return an Effect. For an existing native client, client provides a subscription
import { Effect } from "effect"
import type { Client } from "@neontechspace/fluxerly/effect"
export function installPing(client: Client) {
return client.on("messageCreate", message => {
if (message.author.isBot || message.content !== "!ping") return Effect.void
return client.messages.reply(message, "Pong!")
})
}
Effect describes doing nothing successfully. Returning the reply Effect lets the SDK run and cancel it with the handler. Calling Effect inside each callback would run that work separately from the handler
Run the Complete Bot
This is the bot that fluxerly init --template effect writes. Save it as bot.ts and start it with node --env-file=.env bot.ts, using the .env file from the quick start. The define function holds the bot's options and returns them unchanged, keeping the types of command arguments and the services that handlers require. The runner registers the commands before it connects
import { Effect } from "effect"
import { defineBot, runBot } from "@neontechspace/fluxerly/effect"
// The bot's options without its token, so its test can run them through createTestBot without a token or a network
export const bot = defineBot({
commands: {
prefix: "!",
commands: {
// A failed reply is reported to the log without stopping the bot
ping: { execute: ({ reply }) => reply("Pong!") },
},
},
})
// Running this file starts the bot, while importing it from a test does not.
// A failure that stops the bot, such as a rejected token, is logged and sets a failing exit code
if (import.meta.main)
await Effect.runPromiseExit(runBot({ token: process.env.FLUXER_BOT_TOKEN, processSignals: true, ...bot }))
Send !ping and expect Pong!. Press Ctrl+C to stop the bot. The SDK interrupts it and waits for its cleanup
The last line passes the options and the token to run and runs that program with Effect, which resolves with the program's outcome, called an Exit, instead of rejecting. The line runs only when Node.js runs the file, so a test can import bot without starting it, as the Effect application testing guide shows. The run program reports its own failure: A failure that stops the bot, such as a rejected token, is logged once and sets process to 1, so the Exit needs no further handling. Setting the exit code instead of calling process lets Node.js finish cleanup before the process ends
What the Runner Watches While the Bot Runs
The runner registers the handler before the gateway connects and then watches both the connection and the handler's subscription. A connection failure, or a subscription that closes unexpectedly, stops the bot and waits for cleanup.
A failed handler is reported and not retried, and the bot keeps running. Returning the reply Effect lets the SDK interrupt it when the handler stops.
Keep the token private, including in the environment and in process-manager settings
The small bot guide shows a larger native bot with commands, a permission guard, a cooldown and a welcome message
Report Failures Outside runBot
Application code outside run, such as a one-off job, reports its own failure. A failed Exit holds a Cause, Effect's full record of why the program failed. Pass exit to describe, which prints each error's code, hint and details with credentials masked
import { Effect, Exit } from "effect"
import { createClient, describeError } from "@neontechspace/fluxerly/effect"
export async function announce(token: string, channelId: string) {
const exit = await Effect.runPromiseExit(
Effect.scoped(
Effect.gen(function* () {
const client = yield* createClient({ token })
yield* client.messages.send(channelId, "Maintenance starts in ten minutes")
}),
),
)
if (Exit.isFailure(exit)) {
console.error(describeError(exit.cause))
process.exitCode = 1
}
}
Read the Effect Types
Effect describes the successful value A, expected failures E, and required services R. A client operation can use the same application services as other Effects without the SDK starting a separate runtime
Effect represents expected errors, defects and interruption. A defect is an unexpected fault, and interruption means cancellation. Effect's Cause can retain these together, including cleanup failures. Do not turn every Cause into a retry
Continue with scoped workflows to combine collectors, failure handling and concurrent reads