Tom MacWright

2025@macwright.com

Effect notes: runtimes and logging

I rewrote a method into Effect and now it needs context: how I use ManagedRuntime

So basically we had a method that ended up with the call signature:

getDenoLockfile(key: string, location: string):
	Effect<
		void,
		NotFoundError | PlatformError | UnknownException,
		FileSystem>

Which means that it uses the FileSystem platform for Effect. FileSystem is technically unstable but it’s pretty nice and I think a lot of code uses it. But this presents a challenge in tests: now where my tests said something like

await getDenoLockfile("key0", dest);

Now they need to look like

await Effect.runPromise(getDenoLockfile("key0", dest).pipe(Effect.provide(NodeContext.layer));

This is inelegant. (sidenote: No, I’m not using @effect/vitest yet because it’s missing automatic fixtures and we heavily use them.)

So, how do you make this a little more idiomatic? A custom Runtime, which lets me define a layer in one place and then run Effects with that layer defined:

// At the top, before tests
const runtime = ManagedRuntime.make(NodeContext.layer);

// Now you can run with that runtime and provide NodeContext
await runtime.runPromise(getDenoLockfile("key0", dest));

This took a little while to figure out but there is kind of an example in the docs - I just wish the docs didn’t always start with abstract definitions and they paid a little more attention to common usecases.

There’s also the mental leap of merging layers here: I want both NodeContext and our NodeSdkLive (OpenTelemetry) layers, so this becomes:

export const runtime = ManagedRuntime.make(
  Layer.mergeAll(NodeContext.layer, NodeSdkLive)
);

Effect joys: logging

One thing I really like about Effect is that it does context really well. For example, I wrote a subtle bug and wanted to add some debugging to a function. Usually this means writing debugging code and then ripping it out because even if you use a nice logger (we’ve been using pino) and set the loglevel to debug, turning on debug logs shows debug logs everywhere. But with Effect, you can do something like this:

Effect.gen(function* () {
	yield* Effect.logDebug(`Lockfile existed`);
}).pipe(Logger.withMinimumLogLevel(LogLevel.Debug))

And then when you’re done debugging, keep the logDebug statements but drop the .pipe(Logger.withMinimumLogLevel(LogLevel.Debug)) and that turns logs off. It’s pretty nifty: no longer do I feel like writing nice debug log messages is purely experimental effort, now I can keep them around and turn them on & off via configuration on a per-function basis.


Issues:

Previously: