Avoiding Memory Leaks in JavaScript
Joshua Amaju

Joshua Amaju @joshuaamaju

About: React Engineer | React Native Engineer | Experimentalist

Location:
Lagos, Nigeria
Joined:
Sep 15, 2019

Avoiding Memory Leaks in JavaScript

Publish Date: Mar 3
0 0

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;
}
Enter fullscreen mode Exit fullscreen mode

What's Happening Here?

In this example:

  • operation_might_throw() throws an error, halting further execution of the main 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;
});
Enter fullscreen mode Exit fullscreen mode

What's Different with Effection?

In this version:

  • The long_operation() is spawned as a child task using spawn().
  • 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 that clearTimeout() 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.

Comments 0 total

    Add comment