On This Page

Learn Effect with a Bot

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 peerDependencies.effect in node_modules/@neontechspace/fluxerly/package.json. 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 effect

Check which Effect version the application has installed:

npm list effectpnpm list effectbun why effect

Recheck 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.runPromise 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 sendHello 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:

  1. Effect.gen writes sequential work using a generator function
  2. The yield* expression runs an Effect and returns its successful value. An expected failure skips the remaining steps unless handled
  3. Effect.scoped supplies a cleanup lifetime. When that lifetime ends, Fluxerly closes the client and waits for its owned cleanup
  4. Effect.runPromise runs the program and returns a JavaScript 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.on 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.void describes doing nothing successfully. Returning the reply Effect lets the SDK run and cancel it with the handler. Calling Effect.runPromise 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 defineBot 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 runBot and runs that program with Effect.runPromiseExit, 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 runBot program reports its own failure: A failure that stops the bot, such as a rejected token, is logged once and sets process.exitCode to 1, so the Exit needs no further handling. Setting the exit code instead of calling process.exit() 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 runBot, 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.cause to describeError, 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.Effect<A, E, R> 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