The JavaScript ecosystem evolves at a breakneck pace. Just as you get comfortable with a certain technique, a slew of new methodologies emerges. Some, like TypeScript, gain widespread adoption, while others, such as CoffeeScript, quietly fade away. Each innovation initially stirs excitement, but over time, the community often splits, with detractors eventually spawning their own frameworks. This endless cycle has made me increasingly wary of the latest "magic" frameworks purported to solve all problems. I've shifted from seeking tools as solutions to embracing the understanding of patterns over the constant chase for new technology.
This is why I'm pointing you towards something special for your TypeScript projects, not just another tool but a paradigm that encourages good practices: Effect.
Let's take a look at why you should take the leap.
Colored functions
Have you ever asked yourself what color is your function?
Let me summarize it for you. Imagine you have blue and red functions in your code base. The rule is simple: you can use red functions inside your blue functions but not the other way. Wouldn't that be a nightmare? Now replace blue by "async". Yep, you got function coloring in Javascript.
So how do we fight this coloring thing? If we want to remove colored functions, we would need to create some sort of wrapper that will use a Promise only when needed. Like "Future" or... "Effect"?
import { Effect, pipe } from "effect";
const fibonacci = (a: number): Effect.Effect<number> =>
a <= 1
? Effect.succeed(a)
: pipe(
Effect.all([fibonacci(a - 1), fibonacci(a - 2)]),
Effect.map(([a, b]) => a + b)
);
await Effect.runPromise(fibonacci(10));
The key difference when using Effect
instead of Promise
lies in how concurrency is handled. Effect provides fibers, which are lightweight concurrency structures similar to green threads or goroutines. This feature allows us to perform long-running or asynchronous tasks without blocking the main thread, which can be initiated even within traditionally synchronous functions.
import { Effect, Console, pipe } from "effect";
const longRunningTask = pipe(
Console.log("Start of long running task"),
Effect.delay(1000),
Effect.tap(Console.log("End of long running task"))
);
console.log("Start of program");
Effect.runFork(longRunningTask);
console.log("End of program");
/**
* OUTPUT:
* Start of program
* End of program
* Start of long running task
* End of long running task
*/
While Effect does not eliminate the inherent async/sync distinctions (function coloring) in JavaScript, by using fibers to handle asynchronous operations, it allows synchronous functions to invoke asynchronous effects without becoming asynchronous themselves, thereby mitigating the "coloring" problem to a significant extent.
Typesafe errors
Let's look at this function:
const divide = (a: number, b: number) => a / b;
We just introduced a problem here, we cannot divide by zero. So let's refactor the code a little bit:
const divide = (a: number, b: number) => {
if (b === 0) throw new Error('Cannot divide by zero.');
return a / b;
}
Looks good to you? It is not. Because it is not typesafe. Someone that will want to use your function won't have any idea that your function can throw. This might look trivial with a simple function like this one but when you have dozens of potential errors, it can become a nightmare. Other more mature languages have notions such as Either
or Result
to have typesafe errors. It looks like that:
type Result<T, E> = Ok<T> | Err<E>;
// With something like:
type Ok<T> = { kind: "Ok", data: T };
type Err<E> = { kind: "Err", err: E };
With Effect, you will have that out of the box: Effect<T, E>
. You won't ever have to ask yourself what kind of errors can occur during the run, you can know it directly from the function signature. It also comes with helper functions to recover from errors.
const divide = (a: number, b: number): Effect<number, "DivisionByZeroError"> => {
if (b === 0) return Effect.fail("DivisionByZeroError");
return Effect.succeed(a / b);
}
Newtypes or branded types
You know, looking back at my previous function I realize we could do better.
const divide = (a: number, b: NonZeroNumber) => ...
How do you define NonZeroNumber
though? If you just do type NonZeroNumber = number
it won't prevent people to call it with "0". There is a pattern for that: newtypes. And yeah, Effect supports that too:
import { Brand } from "effect"
type NonZeroNumber = number & Brand.Brand<"NonZeroNumber">
const NonZeroNumber = Brand.refined<NonZeroNumber>(
(n) => n !== 0, // Check if the value is a non-zero number
(n) => Brand.error(`Expected ${n} to be a non-zero number`)
)
This way, you know your function cannot be called with any number: it expects a special type of number which exclude zero.
Dependency injection
If you want to follow the "Inversion of Control" principle, you might want to look into "Dependency Injection". This concept is really simple: a function should have access to what it needs from its own context.
// With a singleton
const read = (filename) => FileReader.read(filename);
// With dependency injection
const read = (reader: FileReader) => (filename) => reader.read(filename);
It's better to do it this way for many reasons such as uncoupling things, allowing for easy testing, having different contexts etc.
While several frameworks assist with this, Effect really crushed it by making it straightforward: put your dependencies as the third parameter of an Effect.
const read = (filename): Effect<File, Error, FileReader> => {
return Effect.flatMap(FileReader, fileReader => {
return fileReader.read(filename);
})
}
Conclusion
There are many other reasons why you should consider Effect. For sure, it's not going to be easy at first, you will have to learn to code differently. But contrary to many frameworks that make you learn "their" way of doing something, Effect actually teaches you good patterns that have made their proofs in other languages. Actually, Effect is heavily inspired by ZIO from Scala which itself has been inspired by Haskell which is still today considered as one of the pinacle of good programming patterns.
Top comments (45)
Thanks for writing about Effect. I do really love the premise of Effect! And I do like your writing style.
But after having checked it out and looked through the code, I feel that you're comparing apples to oranges. (Adding that I don't think using Promises in a non-parallelized setup is a fair comparison. Plus, I would love to see benchmarks that support your claim that it's slower. [This was in the initial version.]) Especially considering - as I understand it - Node handles IO in separate threads while all simple/regular computational work is done in the main event-loop thread. From briefly looking into the code, Effect uses a different mechanism to achieve its effects (over Promises) that hooks into the main event-loop. (Which is why you only see one Promise resolving.)
That said, I do really appreciate the effort you've put into this article and I look forward to playing around with Effect when I have some time. And I really do hope that you keep on writing! (I really do! It was actually a great read!) Thanks again!
I did a quick benchmark and the least to say is that I found interesting results. I confirm that simply putting "async" makes the program way slower. However, it looks like it might be even slower with Effect.
I suspect they might be using iterators which might be slower in JS.
Doesn't change the nature of my argument about colored function, and that Effect is pushing for good patterns. However the part about performance gain is misleading. I removed it for now while I investigate further.
Thanks for reminding me to always double check my assertions!
No problem! Like I said, I tried to be constructive because of the knowledge event horizon - yours and mine - and I did actually like your writing style and didn't want you to be discouraged.
I only recently encountered the coloured functions description and I fully agree with the premise of good patterns. The pattern used in Go also appeals to me. The problem of inconsistency in returns is a pretty universal one in that regard. The fact that I actually spent more than half an hour trying to figure out Effect and how you got your result (AND that I responded) should speak to that! :-)
From what I could tell from briefly looking into it, Effect has its own internal scheduler (at least for some aspects) and uses mechanisms that directly tap into the event-loop (setImmediate/setTimeout).
Appreciate you putting in the extra effort with the benchmarks!
There is no part of me that understands why people seem to think that Typescript, an abstraction layer library written in JS is going to somehow perform better than just the native JS is capable of doing. Even async is syntactic sugar spread across the Promise interface.
This is like saying "my Ford F-250 is slower... than just the F150 I loaded into its bed would be alone!?" My ghast... it is flabbered. 😐
Whether one likes or loathes TS, I think it's reasonable to assert that we can ALL agree it's not intended to offer performance gains of any kind. Its sole and exclusive role in the JS ecosystem is to facilitate (no, to very-opinionatedly-enforce) a specific programming style. The argument being that, for those who see the benefit in same, the overhead is WORTH it.
It is as a consequence of this understanding that I am vehemently-opposed to the inclusion of additional runtime functionality. If they have a great way to obviate "colored" functions or whatever, fine. Release a library. Let people elect to run it. But TS is supposed to be TYPE ENFORCEMENT for JS (a loosely-typed language).
The second you start adding methods that do not exist in JS, you've perverted that purpose. Now people will be UNABLE to seamlessly switch between, because the agreed-upon toolbox differs. It's like randomly altering an API endpoint.
Mark my words here: this is a bad decision.
I think you completely missed my point. To be honest, I am a bit confused by what you are saying.
I was highlighting interesting patterns that are technically completely feasible in pure vanilla JS: the duality sync/async, returning errors instead of throwing, strongly typed primitives, IoC using tagged interfaces...
It appears that the Effect library has implemented all of that, hence the highlight.
Just adding
async
in front a JS function will make it 10 times slower. Effect don't use Promise. It has its own async runtime mechanism. So the question about performance is actually interesting.For the record, here is the result:
And the code I used to benchmark:
Not surprising at all, since the only way to achieve "non-colored" functions is by making everything async, which seems to be what Effect does.
But it's important to note that this is not a relevant performance benchmark in general. Running everything at once is always going to be faster vs slicing up the computation, but in reality you'll have other concerns such as not blocking the event loop (either in node or the browser), allowing other queued tasks to run etc.
Thank you for posting those!
Very good point! I wanted to do the benchmark actually. I promise I will do it asap and update my post.
Disclaimer: I am the author of Effect.
Regarding benchmark it is safe to assume that Effect would be the slowest alternative when compared to any non-boxed code and to JIT optimisable code (e.g. a fibonacci over Promise), which pretty much encompass all the possible micro-benchmarks.
Reality is a bit different from micro benchmarks though, performance issues in JS applications hardly ever come from sync computation but rather from sub-optimal concurrency, given that we mostly write IO-bound apps (e.g. calling databases, apis, etc).
What Effect gives is declarative structured concurrency plus great observability, which will help you spot & fix your code bottlenecks quicker than any alternative.
That said even though Effect will always be slower to non-Effect there are still folks who render their apps at 120fps while using Effect on the rendering happy-path, so I doubt that Effect will be the bottleneck in a specific flow, happy to help with debugging any such case :)
Thanks for the article, it is a great writeup!
THANK you.
Finally, a voice of reason (and from literally THE most qualified person to speak it!).
Personally, I don't really have a dog in this fight either way. Use your tools to most effectively GET THE JOB DONE. If you don't LIKE your tools? You're. A. Programmer. FIX that affront.
But to hear the author stand up and say clearly and concisely "this is what it was made for, and if y'all insist on bullying it into something beyond its intention you're missing the point" is refreshing as hell.
Kudos to you, m'man. Both for putting something out there and for being entirely rational about it. Mad respect.
That's literally why I removed the part about performance from the article.
This sentence might be the most meaningful one in this whole writeup. Though I'm not sure you meant it as such.
I say this because all three: Coffeescript, Typescript, and Effect have a similar purpose and dynamic. My hope for both Typescript and possibly Effect is that they too go the way of Coffeescript.
Coffeescript was created at a time before the modern age of JavaScript. It set the groundwork for many of the improvements that were formalized as part of ES6. It 'quietly faded away' because it was no longer needed. (Though not entirely true as it does feature niceties not in vanilla JS)
Typescript could end up following the same path. Proving that there is something missing from JS that people really do want and pushing forward the inclusion of that in vanilla JS. Typescript as a language could well end up 'fading away' as well.
I haven't played around with Effect yet but I listened to a fairly extensive interview with one of the major players (on Syntax I believe) and he made a compelling argument for its approach. It might well be a library that demonstrates a paradigm that should be supported.
I'll be interested to see what happens with it. Thanks for the writeup.
I like what you say! Not necessarily what I had in mind but I totally agree. Reminds me of the proposal for types (without any compiler or toolchain though) in vanilla JS. Typescript should disappear when Javascript swallows it.
This article showed up on my chrome recommended cards, and I had a look until some point as I am not a frontend developer, I use PHP/Laravel, and I know JS, but also know some C#, Python (a bit) and Rust (a bit)...
I cannot understand why JS is not upgraded... You needing to do all this crazy stuff that was solved ages ago in a lot of other languages... Why can't the JS concortium finally upgrade JS to a "real" high level language like other languages?
I see the community, time after time, re implementing basic stuff... I would really love to see TS already integrated into JS natively... And more...
Long story short: browser wars. That's why we can't have already a language natively work in the browser that is compatible with JS, but is designed and tested well.
You can come up with anything, but if Google, Apple or Mozilla won't implement it, it won't happen. We are still plagued with language features that only work in 1 special browser only.
In my opinion JS and CSS are old enough to be replaced with language purpose-built for scaling enterprises applications: you have all those years of high-usage to know what features should be implemented.
So... your position here is: "I say we break the totality of the existing Internet in favor of a slower, still-ocassionally-buggy alternative that is itself running on the original base you're trying to do away with"?
...and for reasons that amount to, "because everyone should have to code like me, because it's easier for me to understand that way!"?
I have that about right?
Yeah. Good luck with that.
Not entirely sure why you seem to take this abrasive stance towards folks who are simply commenting about the state of the world as it currently is. I think we all feel the frustration with the system as it is. No need to shoot the messenger for it.
Because, yes, the browser companies are effectively gate-keeping progress. But at least nowadays we have more direction through ECMAscript. And yes, we don't really have any great alternatives. (ActionScript could've been great for this, IMHO. Java was great as a concept but ultimately carried too much of a resource burden to make it really viable for adoption on the level that JS could.)
Should we make C#, Go, Erlang, Haskel, Lisp, PERL, PHP, Python and Rust compilers in the browser so that we can extend the ecosystem with all the languages we desire? (We might as well build a WASM runtime C/C++ compiler while we're at it.)
And at the same time, transpiler optimization and run time compiler optimization also haven't stood still.
Various compilers get optimized all the time.
And it's not like WASM is a viable alternative at the moment. Not without taking a performance hit.
So, I am not entirely sure what issue you're exactly trying to get at?
I'm also not sure what Nerdy Deeds wrote. From other comments it seems to be a person not getting what TypeScript is for.
My comment was about the lack of choice; you can't get a more performant language, you can't get a higher level language: you have JS and WASM. I contrast it with any other operating system, where you have a huge array of languages.
My advocacy here is for understanding the following:
Someone give this man a microphone!
I don't think Effect solves colored function problem in your example: you just added another type of functions that return Effect instead of Promise. Of course, we can run effects from regular functions, but we also can do that with promises (but you have to handle the results with .then() and .catch() if you care about them). Or we can get rid of sync functions and promises altogether, leaving only effects, but without effects we can leave only promises as well. I don't think leaving only red functions is the solution to blue/red problem.
BUT(!) Effect actually solves this problem the other way: you can start an effect from any regular function and forget about it, letting another fiber (goroutine equivalent) to handle the result, so a "red" function doesn't change the color of a "blue" function.
Fair point! I'll update the article.
Hmm... and how it is not the case in standard javascript? if you call an async function in a standard function (which is absolutely possible) it will be run in the default event loop (in micro-task-queue) which is pretty much fire and forget. What is the practical difference? If I want to do something with the results, I could easily wrap the call with some async queue function and that's all.
Yeah, you are right. Seems like effect doesn't solve red/blue problem at all.
Yeah, touché. You can technically manage a Promise inside a sync function.
Quick experiment of thought: what if we'd just convert all of our codebase to Promise ? Promise states are hard to keep track of, hard to cancel, you need to catch the error, they have eager evaluation... Also we would still have to use regular functions because of standard methods like let's say
Array.filter
whereas Effect provides standard methods to use both sync and Effect functions.So while I agree that you cannot completely solve the function coloring problem in JS, I still think that Effect helps us closing the gap even if that difference is thin. I will update the article in that sense. Thanks for the comment!
It's a shame we are forced to use this farce of a language where problems that have been solved for decades have to be continually reinvented.
I have good hopes for Rust with wasm on the frontend! Also keeping an eye on Gleam which can sort of compile to JS.
While Effect is heavily influenced by ZIO, as you said, the error handling you described here is a common pattern of handling error without throwing an exception. It is not from ZIO. In Scala, it is handled by
Either
and its subclassesLeft
andRight
One thing to note here about async programming. The keyword
await
is to force a certain timeline to happen. You can useawait
strategically, when you do need the result as input for yet another async function. I don't know howEffect
is implemented, but I think if you usedPromise.all
well in one of your examples you would have had a similar timeframe.(Note, that in the effect Fibonacci example you wrote
n === 1
which does not seem to be correct.)I liked the beginning, I'm not sure JavaScript needs fixing though, the challenge I see is how many things try to hide the power of JS instead of embracing it.
This looks like a lot more layers and I'm mostly excited about the idea of composability, which usually involves simpler components than this. I personally prefer a for loop to the forEach function, which shows how much of native JS I prefer.
if you write a follow up about why you think CoffeeScript didn't pan out, and why TypeScript reigns supreme in a lot of ways, I'd be interesting to read that.
for example: I like the divide by zero handling idea, not 100% sold that the function signature is the right place for it though.
Thanks for posting it was an idea generating read!
Instead of band-aiding JS maybe it's time to just use a real typed language like C# or Java.
Like C# or Java haha, thank you for the good laugh!
And you find this funny why? Typescript is basically modeled after C# but far more difficult to master.
:)
True that. JS community wants some problems to not be fixed. So that the community can keep reinventing and creating contents!
Amen. Give THIS man a microphone.