Side-effect management and code composition are major challenges in programming, particularly in the development of complex applications. Effect-ts is a library that provides elegant solutions to these problems, based on the principles of functional programming.
Let's take a look at how Effect-ts can improve our code, using the example of a post retrieval function.
The initial problem
Let's first consider a classic, trivial implementation of a getPostById
function without Effect-ts:
async function getPostById(id) {
try {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
if (response.status === 404) {
throw new PostNotFoundError(“”);
}
if (!response.ok) {
throw new HttpError(`HTTP error: ${response.status}`);
}
try {
return await response.json();
}
catch (e) {
throw new JsonParseError(“JSON parsing error”)
}
} catch (error) {
if (error instanceof PostNotFoundError) {
console.log(`Post missing. Falling back to a default.`)
return { title: “Sorry, post does not exist !” };
}
else if (error instanceof HttpError) {
console.error(“Exiting.”);
}
throw error;
}
}
This approach has several drawbacks:
- Error handling is verbose and difficult to follow.
- Side effects (network calls, JSON parsing) are mixed in with business logic.
- Error typing is not explicit in the function signature.
The solution with Effect-ts
Here's how we can rewrite this function using Effect-ts:
const fetchWithEffect = (id: number): Effect.Effect<Response, HttpError, never> =>
pipe(
Effect.promise(() => fetch(`https://jsonplaceholder.typicode.com/posts/${id}`)),
Effect.mapError((error) => new HttpError(error.message))
)
const manageResponse = (res: Response): Effect.Effect<Post, HttpError | PostNotFoundError | JsonParseError, never> =>
res.status === 404
? Effect.fail(new PostNotFoundError(""))
: res.ok
? pipe(
Effect.promise(() => res.json()),
Effect.mapError(() => new JsonParseError("JSON parsing error"))
)
: Effect.fail(new HttpError(`Erreur HTTP: ${res.status}`));
const getPostIdWithEffect = (id: number): Effect.Effect<string, Error, never> =>
pipe(
fetchWithEffect(id),
Effect.flatMap(manageResponse),
Effect.catchTags({
HttpError: (error) =>
Effect.logError(`Exiting.`).pipe(
Effect.flatMap(() => Effect.fail(error))
),
PostNotFoundError: () =>
Effect.log(`Post missing. Falling back to a default.`).pipe(
Effect.map(() => ({title: "Sorry, post does not exist !"}))
),
}),
Effect.flatMap((post: Post) => Effect.succeed(post.title)),
Effect.orElseFail(() => new Error("Request failed and no fallback available !"))
);
Advantages of the Effect-ts approach
Safe error typing: Error types are explicitly declared in the effect signature, making the function's contract clearer.
Better handling of side effects: Side effects are encapsulated in
Effect
structures, allowing a clear separation between effect description (what needs to be done) from its actual execution.Easier composition: The use of
pipe
and operators such asflatMap
enable effects to be composed in a readable and maintainable way.Declarative error handling: The use of
catchTags
allows errors to be handled in a more declarative and typed way.Separation of concerns: The logic of data retrieval, error handling and transformation is separated into different functions.
Conclusion
By adopting Effect-ts and the principles of functional programming, we obtain more robust code that's easier to test and maintain.
Explicit side-effect management and functional composition allow us to reason more easily about our code, while benefiting from a powerful type system that catches potential errors right at compile-time.
This approach, although initially more verbose, offers better scalability and maintainability for complex projects, by providing stronger guarantees on the behavior of our code.
Further Exploration
Effect-ts is much more than just a functional library for managing side effects. It's actually a complete ecosystem offering a multitude of tools for the development of modern, robust TypeScript applications.
In addition to effects management and code composition, Effect-ts offers data structures such as Either and Option, as well as tools for data validation, database access and API development. These abstractions enable the creation of more robust and maintainable applications, a topic we may explore further in a future article.
Thanks
Thank you Paul Couthouis for introducing me to this library.
Top comments (0)