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;
The Effect
type takes in three parameters:
-
Requirements
: The data to be executed by the effect, which is stored in aContext
collection. You can also pass innever
as the type parameter if the effect has no requirements -
Error
: Any errors that might occur during execution. You can also choose to pass innever
to indicate that the effect will never fail -
Value
: This represents the success value of an effect. You can passvoid
,never
, or the exact value type you are expecting here. If you pass invoid
, then the success value has no useful information. If you pass innever
, 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);
}
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);
};
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"),
}),
),
);
};
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>
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 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.
Top comments (1)
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?