An unhandled throw in a Cloudflare Worker becomes Error 1101 for the visitor. The Worker graph in the dashboard turns red later. Email from Cloudflare is easy to miss. Teams look for Cloudflare Worker Slack notifications because they want the exception where they already talk.
You do not need a Slack SDK on the edge. Catch the exception, POST AlertHub (key from a Worker secret), then Slack, Telegram, WhatsApp, Discord, or Teams all receive the same critical event.
Use ctx.waitUntil() for the webhook so the 500 can go out immediately and the notify still finishes. Do not destructure waitUntil off ctx — that throws Illegal invocation.
What you should alert on
| Event | Worker handler | Suggested level | Why it matters |
|---|---|---|---|
Uncaught exception in fetch | try/catch + ctx.waitUntil | Critical | Edge route or API Worker died |
Cron (scheduled) failed | same catch on the cron handler | Critical | Nightly job did not run |
Origin fetch() 5xx you already handle | explicit notify | Warning | Origin blinked; Worker stayed up |
| Successful request | omit, or info | Info | History only — do not ping Slack |
Only warning and critical are pushed to external channels. Successful requests should stay info (or be omitted).
Setup (once)
- Create a project at app.thealerthub.com and copy the secret key.
- Store it as a Worker secret, not in
wrangler.toml[vars]:
npx wrangler secret put ALERTHUB_KEY
Locally, put ALERTHUB_KEY=... in .dev.vars (gitignored). The handler reads env.ALERTHUB_KEY.
- Connect destinations in AlertHub. Telegram is enough for a solo maintainer; Slack for the team channel.
Docs: Ingestion API. Same pattern on Netlify: Netlify Function failures.
Alert 1: fetch handler crashed (the default)
Wrap the Worker in try/catch. Pass the notify promise to ctx.waitUntil and attach .catch() so a failed AlertHub call does not mark the invocation as an exception.
const ALERTHUB_URL = 'https://api.thealerthub.com/sendAlert';
function notifyAlertHub(env, { title, description, level = 'critical' }) {
return fetch(ALERTHUB_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ah_key: env.ALERTHUB_KEY,
ah_title: title,
ah_description: description,
ah_level: level,
ah_source: 'Cloudflare Worker',
}),
}).then((res) => {
if (!res.ok) throw new Error(`AlertHub ${res.status}`);
}).catch((notifyError) => {
console.error('AlertHub notify failed', notifyError);
});
}
export default {
async fetch(request, env, ctx) {
try {
const data = await handleRequest(request, env);
return Response.json(data);
} catch (error) {
const stack = error instanceof Error ? error.stack : String(error);
ctx.waitUntil(
notifyAlertHub(env, {
title: 'Cloudflare Worker failed',
description: `${stack}\nurl: ${request.url}`,
}),
);
return new Response('Internal error', { status: 500 });
}
},
};
Deploy, hit a path that throws, then confirm the alert in the dashboard and on your channels.
Alert 2: Shared helper you import everywhere
Queues, Durable Objects, and extra fetch entry points copy-paste badly. Keep one helper and pass env in:
// src/alerthub.js
export function notifyAlertHub(env, error, extra = '') {
const stack = error instanceof Error ? error.stack : String(error);
return fetch('https://api.thealerthub.com/sendAlert', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ah_key: env.ALERTHUB_KEY,
ah_title: 'Cloudflare Worker failed',
ah_description: extra ? `${stack}\n${extra}` : stack,
ah_level: 'critical',
ah_source: 'Cloudflare Worker',
}),
}).catch((notifyError) => console.error('AlertHub notify failed', notifyError));
}
import { notifyAlertHub } from './alerthub.js';
export default {
async fetch(request, env, ctx) {
try {
return await handleRequest(request, env);
} catch (error) {
ctx.waitUntil(notifyAlertHub(env, error, `url: ${request.url}`));
return new Response('Internal error', { status: 500 });
}
},
};
Alert 3: Cron Worker (scheduled)
A failed cron does not return HTTP 500 to a human. Catch it and notify the same way. Do not let the notify promise reject inside waitUntil.
import { notifyAlertHub } from './alerthub.js';
export default {
async scheduled(event, env, ctx) {
try {
await syncFromOrigin(env);
} catch (error) {
ctx.waitUntil(notifyAlertHub(env, error, `cron: ${event.cron}`));
throw error;
}
},
};
throw error keeps the Cloudflare dashboard honest (invocation = exception). If you would rather mark the cron green and only page AlertHub, omit the rethrow.
Ideas to squeeze more value
- Never put
ah_keyin source or[vars]. Secrets viawrangler secret put; local via.dev.vars. - Keep
waitUntilonctx.const { waitUntil } = ctxlosesthisand crashes the Worker. - Include the URL, not cookies. Description is enough to grep Workers Logs; PII does not belong in Slack.
- Preview vs production. Use a second AlertHub project (or
warning) on non-production routes so a staging throw does not page on-call. - Netlify Functions use
process.env.ALERTHUB_KEYinstead ofenv— see Netlify Function failures.
FAQ
Why not only Workers Logs / Tail?
Logs need someone to open them. A Tail session ends. AlertHub is the last hop so WhatsApp, Telegram, and Discord stay in sync.
Can I use process.env.ALERTHUB_KEY?
Only if Node.js compatibility is on. Prefer env.ALERTHUB_KEY from the secret binding — it works in every handler (fetch, scheduled, queue consumers).
Do I have to await the AlertHub fetch before returning 500?
No. ctx.waitUntil(notify…) is the right pattern so the client is not blocked on AlertHub. Still attach .catch() on that promise.
Can I send info on success?
Yes, with "ah_level": "info". It will not fan out to channels.
Next steps
Run npx wrangler secret put ALERTHUB_KEY, paste the try/catch into fetch, force a throw, then connect Slack or Telegram.
Related: Netlify Function failures · Ingestion API · send alerts from Node