DEV Community

Cover image for Avoiding Memory Leaks in JavaScript
Joshua Amaju
Joshua Amaju

Posted on

Avoiding Memory Leaks in JavaScript

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.

Top comments (0)