DEV Community

Cover image for Exploring Effect, a meta-state RxJS-like framework
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Exploring Effect, a meta-state RxJS-like framework

Written by Isaac Okoro
✏️

While TypeScript does a great job of statically typing JavaScript code, certain drawbacks might arise. These drawbacks can include managing the complex nature of asynchronous code, handling types in asynchronous scenarios, and error handling. The Effect library was created as a way to address these drawbacks.

In this tutorial, we’ll get to know Effect, how it works, and why you may benefit from using it. We’ll also compare it to RxJS, a JavaScript library for reactive programming.

What is Effect?

Effect is a functional library for building and composing asynchronous, concurrent, and reactive programs in TypeScript. It focuses on providing a robust and type-safe way to manage side effects in your programs.

A great use case for Effect is building a streaming platform. Ideally, you want to fetch and display recommended content and real-time updates concurrently.

With Effect's async support, you can initiate these tasks without blocking the main thread. This ensures a smooth streaming experience for users while the app handles various asynchronous operations in the background.

The reactive nature of Effect allows you to respond dynamically to events, like new content availability or user interactions, making the streaming platform more responsive and interactive.

Effect helps you structure your code in a way that makes it easier to handle async operations, concurrency, and reactivity while maintaining type safety. It's particularly useful for developers who want to apply functional programming techniques to build reliable and maintainable software in TypeScript.

Why use Effect?

The team behind Effect created this library as an ecosystem of tools that enable you to write TypeScript code in a better and more functional way. You can use Effect to build software in a purely functional manner.

However, Effect’s core function — probably its most unique function — is to provide you with a way to use the type system to track errors and the context of your application. It does this with the help of the Effect type, which is at the core of the Effect ecosystem.

The Effect type allows you to express the potential dependencies that your code will run on, as well as to track errors explicitly. You can check out an example of the Effect type in the code block below:

type Effect<Requirements, Error, Value> = (
  context: Context<Requirements>,
) => Error | Value;
Enter fullscreen mode Exit fullscreen mode

The Effect type takes in three parameters:

  • Requirements: The data to be executed by the effect, which is stored in a Context collection. You can also pass in never as the type parameter if the effect has no requirements
  • Error: Any errors that might occur during execution. You can also choose to pass in never to indicate that the effect will never fail
  • Value: This represents the success value of an effect. You can pass void, never, or the exact value type you are expecting here. If you pass in void, then the success value has no useful information. If you pass in never, then the effect runs forever

Below is an example of an Effect type:

function divide(a: number, b: number): Effect.Effect<never, Error, number> {
  if (b === 0) {
    return Effect.fail(new Error("Cannot divide by zero"));
  }

  return Effect.succeed(a / b);
}
Enter fullscreen mode Exit fullscreen mode

In the code block above, we have a division function that subscribes to the Effect pattern.

We passed never in the Requirements parameter, a type Error in the Error parameter, and a type number in the Value parameter. This means this Effect takes in no requirements, might fail with an error of type Error if the second number is zero, and succeeds with a success value of type number.

When writing TypeScript, we always assume that a function will either succeed or fail. In the case of a failure, we can throw an exception to handle error conditions.

However, what then happens when we are writing code, but we forget to use a try...catch block or throw an exception? Take a look at the example below:

const getData = () => {
  const response = await fetch("fetch someething from a random API");
  const parseResponse = await response.json();
  return dataSchema.parse(parseResponse);
};
Enter fullscreen mode Exit fullscreen mode

The above example makes a fetch request to an API, makes sure that the response is in JSON, and then returns it. The problem with the above code is that each line could crash separately and throw a different error that you may choose to handle differently.

So, how do we fix the above code using the Effect library? Let’s see:

import { Effect, pipe } from "effect";

const getData = (): Effect.Effect<never, Error, Data> => {
  return pipe(
    Effect.tryPromise({
      try: () => fetch("fetch something from a random API"),
      catch: () => new Error("Fetch returned an error"),
    }),

    Effect.flatMap((res) =>
      Effect.tryPromise({
        try: () => res.json(),
        catch: () => new Error("JSON parse cant be trusted also😭"),
      }),
    ),

    Effect.flatMap((json) =>
      Effect.try({
        try: () => dataSchema.parse(json),
        catch: () => new Error("This error is from the data schema"),
      }),
    ),
  );
};
Enter fullscreen mode Exit fullscreen mode

The code above is similar to the example we first discussed. However, this time around, we are taking note of each point where our code might potentially fail and handling each error separately.

This code shows a real-life example of how to use Effect. If you take a look at the code block without the Effect type, we see that the function handles three operations: data fetching, JSON parsing, and dataSchema parsing.

In the example with Effect, we created the type Effect<never, Error, Data> in line three. Now, if you have different error handlers for each specific operation, then you can rewrite the Effect to use those error handlers as follows:

type Effect<never, FetchError | JSONError | DataSchemaError, Data>
Enter fullscreen mode Exit fullscreen mode

With that done, when the getData function runs, you have a specific idea of which of the operations failed and why. You also know that if the getData function passes, it passes with type Data, which you defined above.

Admittedly, the above solution is more verbose than, say, using a try...catch block to wrap the entire function and throw an exception. Even so, Effect ensures that each error is specifically handled, making your code easier to debug.

Features of Effect

So far, we’ve seen how to use the Effect library to build and compose purely functional programs in TypeScript. Now, let’s look at some of its standout features:

  • Concurrency: Effect offers facilities for concurrent programming through the use of fibers, which are lightweight threads of execution that we can schedule independently, enabling efficient and scalable concurrency
  • Error handling: Effect implements a robust error-handling mechanism using functional constructs. This includes the Either data type for explicit error handling and the ability to define error channels within effects
  • Resource management: Effect provides a structured approach to managing resources. The library ensures that resources are acquired and released safely, preventing resource leaks and improving resource management
  • Composability: Effect emphasizes composability and modularity. Since it’s easy to compose effects, you can also easily build complex programs from smaller, reusable components
  • Type safety: Effect leverages the TypeScript type system extensively to provide strong type safety. This helps catch many errors at compile-time, reducing the likelihood of runtime issues

These features, alongside the actual Effect type we saw in action earlier, all make Effect a great tool for developers who want to manage side effects in async, concurrent, and reactive TypeScript programs in a robust and type-safe manner.

Pros and cons of using Effect

Using Effect in your projects comes with several benefits, along with a couple of drawbacks to keep in mind.

Some of the pros of Effect include:

  • Structured asynchronous programming: Effect introduces a structured approach to handling asynchronous operations. This enables developers to compose and manage asynchronous code in a declarative and functional manner
  • Gradual adoption: An added benefit of using the Effect library is that it can be gradually adopted into TypeScript projects, allowing developers to choose how and if they want to continue with the library without imposing a radical shift in development practices
  • Maintenance and debugging ease: Using Effect allows you to maintain and easily debug your code because the predictable nature of Effect code can contribute to easier maintenance and debugging

Meanwhile, some of the drawbacks of using Effect include:

  • Learning curve: The learning curve for Effect is quite steep, as Effect introduces functional programming concepts to TypeScript, which might pose a challenge for developers who are not familiar with it. However, since you can adopt Effect gradually in your projects, you can take your time to learn how best to apply effects to your TypeScript programs
  • Ecosystem maturity: While Effect has an active community, the ecosystem might not be as mature or extensive as some other libraries or frameworks. This could impact the availability of documentation, third-party libraries, and resources

It’s important to consider these factors before choosing whether or not to adopt Effect into your TypeScript projects.

How does Effect compare to RxJS?

RxJS is a library for reactive programming using Observables, which is a powerful and versatile way to handle asynchronous and event-based programming. RxJS is featured around a reactive programming paradigm that uses Observables, Observers, and Subjects to handle events.

So, how does RxJS compare to Effect? Below is a table that compares the features of RxJS and Effect:

Features Effect RxJS
Programming paradigm Functional programming Reactive programming
Main features Effects and fibers Observables
Error handling Yes Yes
Pros Type safety: robust error handling, promotes testability Composability, state management, and error handling
Cons Steep learning curve, verbose code base Requires data immutability, makes writing tests complex
Community and ecosystem Has an active community with a growing ecosystem Has an active community with a well-established ecosystem

Effect and RxJS are both important libraries. Here are some tips to know which option to choose for different situations:

  • Choose a library that suits your project needs
  • Choose Effect when strong type safety, composability, and functional programming are key
  • Choose RxJS for asynchronous and event-based projects

While these libraries can be used together, this approach is not advised, as it can lead to potential complexity.

Conclusion

Throughout this article, we explored Effect, a powerful library for writing TypeScript. Effect provides a robust set of abstractions and features that contribute to code reliability, maintainability, and predictability.

We looked at the Effect type, which is at the core of Effect, as well as the various features of Effect. We also compared this library to RxJS to better understand when and how to strategically use each in your TypeScript projects.

Have fun using Effect in your next project. You can find out more in the Effect documentation.


LogRocket: Full visibility into your web and mobile apps

LogRocket Signup

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

Try it for free.

Top comments (1)

Collapse
 
dariomannu profile image
Dario Mannu

Why do you say testing Rx streams is complex and a con? With the like of rxjs-marbles you can cover a test of any complexity in 2 lines of ASCII-art code: stream in, stream out. I've never seen anything more powerful and convenient than that, actually.

On the other hand, the learning curve is probably just as steep for both?