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.
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.
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 a Context
collection. You can also pass in never
as the type parameter if the effect has no requirementsError
: Any errors that might occur during execution. You can also choose to pass in never
to indicate that the effect will never failValue
: 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 foreverBelow 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.
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:
Either
data type for explicit error handling and the ability to define error channels within effectsThese 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.
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:
Meanwhile, some of the drawbacks of using Effect include:
It’s important to consider these factors before choosing whether or not to adopt Effect into your TypeScript projects.
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:
While these libraries can be used together, this approach is not advised, as it can lead to potential complexity.
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 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.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowExplore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
Build a real-time image background remover in Vue using Transformers.js and WebGPU for client-side processing with privacy and efficiency.
Optimize search parameter handling in React and Next.js with nuqs for SEO-friendly, shareable URLs and a better user experience.