Skip to content
Warlock.js v4

Ship errors to Slack (or any external channel)

You want error (and maybe warn) entries to land in a Slack channel so the team sees production failures without tailing a file. The logger doesn’t bundle network sinks, but a custom channel is ~15 lines: extend LogChannel, filter to the levels you care about, and POST.

src/channels/slack-log.ts
import { LogChannel, type BasicLogConfigurations, type LoggingData } from "@warlock.js/logger";
// Extend the base config so the inherited `levels`, `filter`, and `redact`
// options are part of the type the channel accepts.
type SlackConfig = BasicLogConfigurations & {
webhookUrl: string;
};
export class SlackLog extends LogChannel<SlackConfig> {
public name = "slack";
public async log(data: LoggingData) {
// Inherit the levels whitelist + filter predicate for free.
if (!this.shouldBeLogged(data)) {
return;
}
const { type, module, action, message } = data;
await fetch(this.config("webhookUrl"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `:rotating_light: *[${type.toUpperCase()}]* \`${module}\`\`${action}\`\n${message}`,
}),
});
}
}
src/logger.ts
import { log, ConsoleLog, FileLog } from "@warlock.js/logger";
import { SlackLog } from "./channels/slack-log";
log.setChannels([
new ConsoleLog(), // everything to the terminal
new FileLog({ chunk: "daily" }), // everything to disk
new SlackLog({
webhookUrl: process.env.SLACK_WEBHOOK_URL!,
levels: ["error", "warn"], // ← only these reach Slack
}),
]);

The levels: ["error", "warn"] whitelist means debug / info / success never hit the network — shouldBeLogged drops them before your fetch runs. The console and file channels still see everything; filtering is per channel.

Don’t let a flaky webhook crash your app

Section titled “Don’t let a flaky webhook crash your app”

A channel’s log() is fired without being awaited by the logger, so an unhandled rejection from fetch could take down the process. Swallow transport failures inside the channel:

public async log(data: LoggingData) {
if (!this.shouldBeLogged(data)) {
return;
}
try {
await fetch(this.config("webhookUrl"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: this.format(data) }),
});
} catch {
// A logging sink must never be the thing that crashes the app.
// Drop the Slack delivery; the console + file channels still recorded it.
}
}

Slack is an external destination — scrub secrets harder for it than for your local file. Per-channel redact paths are additive on top of the logger floor (a channel can redact more, never less):

new SlackLog({
webhookUrl: process.env.SLACK_WEBHOOK_URL!,
levels: ["error"],
redact: {
paths: ["context.user.email", "context.token"],
},
});

See Redaction for how the floor and per-channel paths combine.

  • Custom Channel — the full LogChannel contract, the init() hook, and flushSync
  • Filtering — the levels whitelist and filter predicate
  • Redaction — additive per-channel redaction