I’ve been playing around with Cloudflare workers recently, which is a serverless compute platform with a number of handy capabilities and a pretty decent free tier.

One of the things you can do with workers is to have them execute on a schedule defined by a cron expression. Unfortunately, if you’d like to get the logs for your worker you’ll either need to do a bit of work to set up Logpush or be streaming the logs via CLI or in the console at the time your worker is running.

For my purposes, I didn’t want to have to actively check logs to figure out what my worker was doing, but wanted to be occasionally notified under certain circumstances. I figured that sending an email from the worker in those circumstances would be a better approach, and simple to set up, it’s a first-class feature within workers, just check out the docs.

When I’ve been developing my worker I’ve been running it locally using wrangler, as is set up when generating a project following the instruction in the quickstart guide. I don’t deploy my worker to test every change, that sounds absurd to me.

When attempting to test the email sending capability locally I ran in to the following error;

service core:user:my-project: Uncaught Error: No such module "cloudflare-internal:email".
  imported from "cloudflare:email"

You will get this error even if you’re not triggering the code to send the email as the workerd runtime tries to start up. The fix for this wound up being pretty simple, but I couldn’t find it documented anywhere, so here it is. Don’t load that module if you don’t have to.

// Within a function that I call to send an email
const { EmailMessage } = await import('cloudflare:email');

const emailMessage: EmailMessage = new EmailMessage(
  'from@example.com',
  'to@example.com',
  msg.asRaw()
);

This on its own isn’t enough however, because if you run this code locally it will still fail. You’ll need to work out what to do when you’d like to send an email, I just print to console locally and ensure that the await import('cloudflare:email') is only executed in a deployed environment.

I have the following in my wrangler.toml;

[env.local]
vars = { ENVIRONMENT = "local" }

I updated the dev script in package.json to start up using that environment;

"dev": "wrangler dev --env local"

Inside my index.ts is an interface that defines what will be passed to the handlers at runtime, and one of the things that will be passed in is that new ENVIRONMENT variable.

export interface Env {
  // Learn more about setting up Bindings here; https://developers.cloudflare.com/workers/configuration/bindings/#email-bindings
  NOTIFICATION_EMAIL: SendEmail;
  ENVIRONMENT: 'local' | 'staging' | 'production';
}

Now when I run my worker locally I can check the env.ENVIRONMENT property that is passed to my handler, and do something like (but more complicated than) the following;

if(env.ENVIRONMENT === 'local') {
  console.log('Would send email\n' + emailBody)
} else {
  // Code that would run in a deploy environment
  const { EmailMessage } = await import('cloudflare:email');
  ...
}