In JavaScript, it's easy to unintentionally create memory leaks and dangling promises, especially when dealing with asynchronous operations. This is often due to the way errors and non-blocking tasks are handled. Unfortunately, the tools available don't address or provide straightforward solutions to these problems.
Let's take a look at an example that illustrates this issue:
function operation_might_throw() {
// Simulates an operation that may throw an error
throw new Error("An unexpected error occurred");
}
function long_operation() {
return new Promise(resolve => {
setTimeout(() => {
console.log("This will still log to the console");
resolve(null);
}, 1000);
});
}
async function main() {
// Start a non-blocking asynchronous operation
const promise = long_operation();
// Perform another operation which might throw an error
operation_might_throw();
// Await the non-blocking operation
await promise;
}
What's Happening Here?
In this example:
-
operation_might_throw()
throws an error, halting further execution of themain
function. - However, the
long_operation()
continues to run in the background and logs to the console even after the error. - This happens because the promise is non-blocking and isn't aware of the error in the parent scope.
The Problem
This example demonstrates how easy it is to write memory-unsafe and hard-to-debug asynchronous code in JavaScript. Here, the long_operation()
continues to run even though it's no longer needed.
The challenge is: How do we ensure that long-running operations clean up once they're no longer needed? In this case, the cleanup should happen because of an error in the parent scope.
JavaScript's native async/await
doesn't provide a straightforward way to handle this scenario. This is where Effection comes in.
What is Effection?
Effection is a library that enables structured concurrency in JavaScript, ensuring that asynchronous tasks are managed within a clear and predictable scope.
Rewriting the Example with Effection
Here's how we can rewrite the previous example using Effection:
import { spawn, call, ensure, main } from "effection";
function* long_operation() {
let timeout: number;
// Ensure the timeout is cleared, preventing memory leaks
yield* ensure(() => clearTimeout(timeout));
const promise = new Promise(resolve => {
timeout = setTimeout(() => {
console.log("This will NOT log to the console if an error occurs");
resolve(null);
}, 1000);
});
// Yield to the promise, allowing other tasks to run
return yield* call(promise);
}
main(function*() {
// Start a non-blocking long operation as a child task
const task = yield* spawn(long_operation);
// Perform another operation which might throw an error
operation_might_throw();
// Await the completion of the child task
yield* task;
});
What's Different with Effection?
In this version:
- The
long_operation()
is spawned as a child task usingspawn()
. - If
operation_might_throw()
throws an error, Effection automatically cleans up the child task. - This prevents the console log from occurring, as the long-running operation is canceled.
-
ensure()
guarantees thatclearTimeout()
is called, preventing memory leaks.
Why Use Effection?
Effection's structured concurrency model ensures that:
- Asynchronous operations are organized in a predictable hierarchy.
- Child tasks are automatically canceled when the parent goes out of scope (fails or completes).
- Memory leaks and dangling promises are prevented by automatically cleaning up resources.
Top comments (0)