Hazel

5. Error Handling

Handle errors gracefully and provide user feedback

When building bots, errors can occur - API failures, validation issues, or unexpected inputs. Let's handle them gracefully.

Using withErrorHandler

The SDK provides a withErrorHandler utility that:

  1. Catches any error in your command handler
  2. Logs the error with context
  3. Sends a user-friendly message to the channel
yield *
	bot.onCommand(
		AddTaskCommand,
		(ctx) =>
			Effect.gen(function* () {
				// Your command logic here
				tasks.push(ctx.args.title)
				yield* bot.message.send(ctx.channelId, `Added: ${ctx.args.title}`)
			}).pipe(bot.withErrorHandler(ctx)), // <-- Wrap with error handler
	)

If anything fails, the user sees:

An unexpected error occurred. Please try again.

Manual Error Handling

For more control, use Effect's error handling:

yield *
	bot.onCommand(AddTaskCommand, (ctx) =>
		Effect.gen(function* () {
			tasks.push(ctx.args.title)
			yield* bot.message.send(ctx.channelId, `Added: ${ctx.args.title}`)
		}).pipe(
			Effect.catchAll((error) =>
				Effect.gen(function* () {
					yield* Effect.logError("Failed to add task", { error })
					yield* bot.message.send(
						ctx.channelId,
						"Sorry, I couldn't add that task. Please try again.",
					)
				}),
			),
		),
	)

Handling Specific Errors

Use Effect.catchTag to handle specific error types:

yield *
	bot.message.send(channelId, content).pipe(
		Effect.catchTag("HttpClientError", (error) =>
			Effect.gen(function* () {
				yield* Effect.logError("HTTP error", { error })
				// Maybe retry or notify the user
			}),
		),
	)

Validation Errors

The SDK validates command arguments automatically. If validation fails, the command handler won't run. To provide custom validation:

yield *
	bot.onCommand(AddTaskCommand, (ctx) =>
		Effect.gen(function* () {
			// Custom validation
			if (ctx.args.title.length > 100) {
				yield* bot.message.send(ctx.channelId, "Task title too long! Keep it under 100 characters.")
				return
			}

			if (tasks.includes(ctx.args.title)) {
				yield* bot.message.send(ctx.channelId, "That task already exists!")
				return
			}

			tasks.push(ctx.args.title)
			yield* bot.message.send(ctx.channelId, `Added: ${ctx.args.title}`)
		}).pipe(bot.withErrorHandler(ctx)),
	)

Error Logging

Use Effect's logging for structured error logs:

yield *
	Effect.logError("Operation failed", {
		error,
		context: {
			command: ctx.commandName,
			userId: ctx.userId,
			channelId: ctx.channelId,
		},
	})

Complete Task Bot

Here's the final Task Bot with proper error handling:

import { Effect, Schema } from "effect"
import { Command, CommandGroup, runHazelBot } from "@hazel/bot-sdk"

const tasks: string[] = []

const AddTaskCommand = Command.make("task-add", {
	description: "Add a new task",
	args: { title: Schema.String },
	usageExample: "/task-add Buy groceries",
})

const ListTasksCommand = Command.make("task-list", {
	description: "List all tasks",
})

const ClearTasksCommand = Command.make("task-clear", {
	description: "Clear all tasks",
})

const commands = CommandGroup.make(AddTaskCommand, ListTasksCommand, ClearTasksCommand)

runHazelBot({
	commands,
	setup: (bot) =>
		Effect.gen(function* () {
			yield* Effect.log("Task Bot is starting...")

			// Add task with validation and error handling
			yield* bot.onCommand(AddTaskCommand, (ctx) =>
				Effect.gen(function* () {
					// Validate title length
					if (ctx.args.title.length > 100) {
						yield* bot.message.send(
							ctx.channelId,
							"Task title too long! Keep it under 100 characters.",
						)
						return
					}

					// Check for duplicates
					if (tasks.includes(ctx.args.title)) {
						yield* bot.message.send(ctx.channelId, "That task already exists!")
						return
					}

					// Add the task
					tasks.push(ctx.args.title)
					const msg = yield* bot.message.send(ctx.channelId, `Added: ${ctx.args.title}`)
					yield* bot.message.react(msg, "✅")
				}).pipe(bot.withErrorHandler(ctx)),
			)

			// List tasks with error handling
			yield* bot.onCommand(ListTasksCommand, (ctx) =>
				Effect.gen(function* () {
					if (tasks.length === 0) {
						yield* bot.message.send(ctx.channelId, "No tasks yet! Use /task-add to create one.")
						return
					}
					const list = tasks.map((t, i) => `${i + 1}. ${t}`).join("\n")
					yield* bot.message.send(ctx.channelId, `**Tasks (${tasks.length}):**\n${list}`)
				}).pipe(bot.withErrorHandler(ctx)),
			)

			// Clear tasks with error handling
			yield* bot.onCommand(ClearTasksCommand, (ctx) =>
				Effect.gen(function* () {
					const count = tasks.length
					if (count === 0) {
						yield* bot.message.send(ctx.channelId, "No tasks to clear!")
						return
					}

					const msg = yield* bot.message.send(ctx.channelId, "Clearing tasks...")
					tasks.length = 0
					yield* bot.message.update(msg, `Cleared ${count} task${count === 1 ? "" : "s"}!`)
					yield* bot.message.react(msg, "🗑️")
				}).pipe(bot.withErrorHandler(ctx)),
			)

			// React to task mentions (with error catching)
			yield* bot.onMessage((message) =>
				Effect.gen(function* () {
					if (message.content.toLowerCase().includes("task")) {
						yield* bot.message.react(message, "📝")
					}
				}).pipe(
					Effect.catchAll((error) => Effect.logWarning("Failed to react to message", { error })),
				),
			)
		}),
})

What's Next?

Congratulations! You've built a complete Task Bot with:

  • Message handling and reactions
  • Type-safe slash commands
  • Message operations (send, reply, update, react)
  • Proper error handling

On this page