Skip to content

Handling Errors

So far, we’ve been implicitly ignoring errors. We’ve used try? to suppress errors and try! to crash the program if an error occurs. But this means we never handle errors gracefully. In this section, we’ll learn how to throw and catch errors with DDBKit’s error handling system.

This is preferred over the usual do {} catch {_ in} like we’re used to since we can avoid nesting and keep our code clean.

Throwing Errors

Change any instances of try? to try and try! to try. This will make the function throw an error if it encounters one. You can throw errors with the throw keyword. Errors thrown from commands will propagate up to the command handler.

Command("failable") { i in
struct Egg: Decodable {
var gm: String
}
let data = "{}".data(using: .utf8)!
_ = try JSONDecoder().decode(Egg.self, from: data)
}

Running your bot now will result in the following:

Terminal window
2024-10-25T11:51:18+0100 notice GatewayManager : connectionId=1 gateway-id=1 [DiscordGateway] Received ready notice. The connection is fully established
[Uncaught Error] This command failed oh no who could've guessed
Interaction( ... )

Throwing errors from a command will log an uncaught error, caught by the error handler. It only does this if you don’t provide any way to avoid propagating the error to this level. This is useful for debugging, but you should probably handle errors more gracefully in production.

Catching Errors

DDBKit provides two ways of catching errors to best suit your needs. You can catch errors at the command scope or at the global scope.

Global Scope

The DiscordBotApp protocol defines a method called boot() async throws. This method is called before bot is started. This is provided as a way to register extensions and other setup code. You can use this method to catch errors at the global scope.

@main
struct MyNewBot: DiscordBotApp {
init() async {
// ...
}
func boot() async throws {
AssignGlobalCatch { error, i in
try? await i.respond {
Message {
MessageEmbed {
Title("Your command ran into a problem")
Description {
Text(error.localizedDescription)
}
}
.setColor(.red)
}
}
}
}
var body: [any BotScene] {
Command("failable") { i in
struct Egg: Decodable {
var gm: String
}
let data = "{}".data(using: .utf8)!
_ = try JSONDecoder().decode(Egg.self, from: data)
}
}
var bot: Bot
var cache: Cache
}

Running the new /failable command will result in the following:

User used /failable
MyNewBot Bot 10/23/2024
Your command ran into a problem
The data couldn’t be read because it is missing.

Command Scope

You can also catch errors at the command scope. This is useful if you want to handle errors differently for different commands.

Command("failable") { i, _, _ in
struct Egg: Decodable {
var gm: String
}
let data = "{}".data(using: .utf8)!
_ = try JSONDecoder().decode(Egg.self, from: data)
}
.catch { error, interaction
try? await interaction.respond {
Message {
MessageEmbed {
Title("Failable failed, who could've guessed")
Description {
Text(error.localizedDescription)
}
}
.setColor(.red)
}
}
}

I feel this is relatively self-explanatory. The catch closure is called when an error is thrown from the command.

User used /failable
MyNewBot Bot 10/23/2024
Failable failed, who could’ve guessed
The data couldn’t be read because it is missing.

Propagating errors

You can propagate errors from command scope to global scope by rethrowing the error. This is useful if you want to handle errors differently at the global scope after some implicit work or just transforming the error at command scope

Command("failable") { i in
struct Egg: Decodable {
var gm: String
}
let data = "{}".data(using: .utf8)!
_ = try JSONDecoder().decode(Egg.self, from: data)
}
.catch { error, _ in
// ...
let transformedError = /* new error or something (maybe more user friendly? who knows) */
throw transformedError
// this will now be caught by global scope (if any)
}