Thinking in Effection
When we say that Effection is "Structured Concurrency and Effects for Javascript" we mean three things:
- No operation runs longer than its parent.
- Every operation exits fully.
- It's just JavaScript, and except for the guarantees derived from (1) and (2), it should feel familiar in every other way.
Developing a new intuition about how to leverage Structured Concurrency, while leaning on your existing intuition as a JavaScript developer will help you get the most out of Effection and have you attempting things that you would never have even dreamed before.
No operation runs longer than its parent.
In JavaScript, developers rarely have to think about memory allocation because memory lifetime is bound to the scope of the function that it was allocated for. When a function is finished, its scope is torn down and all of the memory allocated to variables in function's scope are released safely. Binding memory management to scope gives developers the freedom to focus on their application instead worrying about leaking memory.
Structured concurrency establishes the same relationship between scope and asynchrony. Every Effection operation is bound to the lifetime of its parent. Because of this, Effection automatically tears down child operations when the parent operation completes or is halted. Like with memory management, binding asynchrony to scope frees developers to focus on writing their applications instead of worrying about where and when to run asynchronous cleanup.
The key to achieving this freedom is to make the following mental shift about the natural lifetime of an asynchronous operation.
before
An asynchronous operation will run as long as it needs to
after
An asynchronous operation runs only as long as it's needed.
Effection provides this shift. Whenever an operation completes, none of its child operations are left around to pollute your runtime.
💡 You might assume that Effection makes all operations asynchronous which is incorrect. Effection is built on Deliminated Continuations which allows us to treat synchronous and asynchronous operations uniformly without forcing synchronous operations to run asynchronously.
Every operation exits fully.
We expect synchronous functions to run completely from start to finish.
function main() {
try {
fn()
} finally {
// code here is GUARANTEED to run
}
}
Knowing this makes code predictable. Developers can be confident that their functions will either return a result or
throw an error. You can wrap a synchronous function in a try/catch/finally
block to handle thrown errors. The
finally
block can be used to perform clean up after completion.
However, the same guarantee is not provided for async functions.
async function main() {
try {
await new Promise((resolve) => setTimeout(resolve, 100,000));
} finally {
// code here is NOT GUARANTEED to run
}
}
await main();
Once an async function begins execution, the code in its finally{}
blocks may never get
a chance to run, and as a result, it is difficult to write code that always cleans up after itself.
This hard limitation of the JavaScript runtime is called the Await Event Horizon, and developers experience its impact on daily basis. For example, the very common EADDRINUSE error is caused by a caller not being able to execute clean up when a Node.js process is stopped.
By contrast, Effection does provide this guarantee.
import { main, action } from "effection";
await main(function*() {
try {
yield* action(function*(resolve) { setTimeout(resolve, 100,000) });
} finally {
// code here is GUARANTEED to run
}
});
When executing Effection operations you can expect that they will run to completion; giving every operation an opportunity to clean up. At first glance, this might seem like a small detail but it's fundamental to writing composable code.
It's just JavaScript
Effection is designed to provide Structured Concurrency guarantees using common JavaScript language constructs such as
let
, const
, if
, for
, while
, switch
and try/catch/finally
. Our goal is to allow JavaScript developers to
leverage what they already know while gaining the guarantees of Structured Concurrency. You can use all of these
constructs in an Effection function and they'll behave as you'd expect.
However, one of the constructs we avoid in Effection is the syntax and semantics of async/await
.
This is because async/await
is not capable of modeling structured concurrency. Instead, we make
extensive use of generator functions which are a core feature of JavaScript and are supported by all browsers
and JavaScript runtimes.
Finally, we provide a handy Effection Rosetta Stone to show how Async/Await concepts map into Effection APIs.