Some coding patterns stay true regardless of programming language.
No, this is not about OOP, functional programming, design patterns, or anything like that 💁🏼♂️
This is all about shared problems with every codebase, and how to solve those problems:
- Error handling (
Either
/Option
)- Dependency management (Dependency injection)
- Debugging (Logging and observability)
These were the same when I started with PHP, with Java for Android, Dart, Typescript, and all.
Let me share this with you 👇
Tech stack
- Effect: I am exploring these principles and how to solve them with Effect lately. The same ideas can (and will) be applied to other programming languages
Setup
This week I explored more of the Effect API.
Principle 1: any usable language must provides tooling (documentation, IDE support, types, debugging)
This is why working with Typescript and Dart is great: the IDE support is next level 🚀
Get started
I rewrote some native Typescript code to Effect to learn the differences.
Principles 2: Error handling requires
Either
andOption
(try/catch is flawed)
Errors cannot be ignored: they must be explicit at the type level.
Effect.Effect<void, PlatformError | FileError, Fs.FileSystem> // 👈 With Effect
Promise<void> // 👈 Plain Typescript
Effect collects all possible errors, you know exactly what can go wrong. You can then handle all the errors in one place.
"If it complies it works" 🪄
There is more 🤩
Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.
Implementation
Solving error handling is as simple as this 👇
export type Either<E, A> = Left<E, A> | Right<E, A>
You only need to be explicit: sometimes things go wrong (Left
) and sometimes they work (Right
).
This removes any try/catch, throw, exceptions, or whatever.
Scale complexity: Dependency Injection
Second (major) problem: organize code dependencies to loose coupling and simplify testing.
Principle 3: interfaces and dependency injection
Effect uses services and layers: define interfaces, and only at the very end provide concrete implementations.
export interface JsonParseService {
readonly parse: (
source: string
) => Effect.Effect<JsonString, JsonParseError>;
}
export const JsonParseService = Context.GenericTag<JsonParseService>(
"@app/JsonParseService"
);
In the above code there is no mention of "how" this is implemented.
Nonetheless, we can still use the service, since the interface is enough to know what this does:
const program = (body: string) =>
Effect.gen(function* (_) {
const json = yield* _(JsonParse.JsonParseService);
const idOrUrlJson = yield* _(json.parse(body));
return yield* _(idOrUrlJson, parseZod(zodSchema));
});
We can then "inject" the implementation only at the end:
const JsonParseServiceLive = Layer.succeed(
JsonParseService,
JsonParseService.of({
parse: (source) =>
Effect.try({
try: () => JsonString(JSON.parse(source)),
catch: (error) => new JsonParseError({ source, error }),
}),
})
);
const program = (body: string) =>
Effect.gen(function* (_) {
const json = yield* _(JsonParse.JsonParseService);
const idOrUrlJson = yield* _(json.parse(body));
return yield* _(idOrUrlJson, parseZod(zodSchema));
}).pipe(
Effect.provide(JsonParse.JsonParseServiceLive)
);
This allows full control over each implementation, and more flexibility for testing ✅
Logging and observability
How do I make sure everything works as expected?
Principle 4: debugging/logging/observability are not afterthoughts, they are core to every codebase
Effect provides a full Logger
module for message reporting (fully customizable with dependency injection 🛠️).
Effect.gen(function* (_) {
const fs = yield* _(Fs.FileSystem);
const gzip = yield* _(Gzip.Gzip);
const existsExtractedFilename = yield* _(fs.exists(extractedFilename));
if (useCachedFile && existsExtractedFilename) {
return yield* _(
Effect.logDebug(`using cached file "${extractedFilename}"`)
);
}
yield* _(Effect.logDebug(`extracting ${gzipFilename}...`));
yield* _(gzip({ gzipFilename, extractedFilename }));
})
Often times logging is not enough. We need full observability.
Effect provides APIs for Metrics and Tracing:
- Tracking a Value Over Time
- Request Counts
- Error Counts
- Memory Usage
I have still a lot to explore on these APIs 🤓
This is your starting point to start converting your codebase to Effect 💡
Takeaways
- Every programming language shares similar challenges
- Some principles stay true regardless of your programming language or framework
- The main problem is managing complexity: explicit errors and dependencies using types helps scaling
- Effect is spearheading a new paradigm, but this applies to other languages as well 💡
I am flying to Vienna this week for the Effect Days ✈️
Next week is all about the conference and all the new innovations, learnings, and takeaways from it.
Trust me: you don't want to miss this 🤝
See you next 👋