A RetroSearch Logo

Home - News ( United States | United Kingdom | Italy | Germany ) - Football scores

Search Query:

Showing content from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Resource_management below:

JavaScript resource management - JavaScript

JavaScript resource management

This guide talks about how to do resource management in JavaScript. Resource management is not exactly the same as memory management, which is a more advanced topic and usually handled automatically by JavaScript. Resource management is about managing resources that are not automatically cleaned up by JavaScript. Sometimes, it's okay to have some unused objects in memory, because they don't interfere with application logic, but resource leaks often lead to things not working, or a lot of excess memory usage. Therefore, this is not an optional feature about optimization, but a core feature to write correct programs!

Note: While memory management and resource management are two separate topics, sometimes you can hook into the memory management system to do resource management, as a last resort. For example, if you have a JavaScript object representing a handle of an external resource, you can create a FinalizationRegistry to clean up the resource when the handle is garbage collected, because there is definitely no way to access the resource afterwards. However, there is no guarantee that the finalizer will run, so it's not a good idea to rely on it for critical resources.

Problem

Let's first look at a few examples of resources that need to be managed:

Here is one concrete example, using a readable stream:

const stream = new ReadableStream({
  start(controller) {
    controller.enqueue("a");
    controller.enqueue("b");
    controller.enqueue("c");
    controller.close();
  },
});

async function readUntil(stream, text) {
  const reader = stream.getReader();
  let chunk = await reader.read();

  while (!chunk.done && chunk.value !== text) {
    console.log(chunk);
    chunk = await reader.read();
  }
  // We forgot to release the lock here
}

readUntil(stream, "b").then(() => {
  const anotherReader = stream.getReader();
  // TypeError: ReadableStreamDefaultReader constructor can only
  // accept readable streams that are not yet locked to a reader
});

Here, we have a stream that emits three chunks of data. We read from the stream until we find the letter "b". When readUntil returns, the stream is only partially consumed, so we should be able to continue to read from it using another reader. However, we forgot to release the lock, so although reader is no longer available, the stream is still locked and we cannot create another reader.

The solution in this case is straightforward: call reader.releaseLock() at the end of readUntil. But, a few issues still remain:

You see how a seemingly benign task of calling releaseLock can quickly lead to nested boilerplate code. This is why JavaScript provides integrated language support for resource management.

The using and await using declarations

The solution we have is two special kinds of variable declaration: using and await using. They are similar to const, but they automatically release the resource when the variable goes out of scope as long as the resource is disposable. Using the same example as above, we can rewrite it as:

{
  using reader1 = stream1.getReader();
  using reader2 = stream2.getReader();

  // do something with reader1 and reader2

  // Before we exit the block, reader1 and reader2 are automatically released
}

Note: At the time of writing, ReadableStreamDefaultReader does not implement the disposable protocol. This is a hypothetical example.

First, notice the extra braces around the code. This creates a new block scope for the using declarations. Resources declared with using are automatically freed when they go out of the scope of using, which, in this case, is whenever we are exiting the block, either because all statements have executed, or because an error or return/break/continue was encountered somewhere.

This means using can only be used in a scope that has a clear lifetime—namely, it cannot be used at the top level of a script, because variables at the top level of a script are in scope for all future scripts on the page, which practically means the resource can never be freed if the page never unloads. However, you can use it at the top level of a module, because the module scope ends when the module finishes executing.

Now we know when using does cleanup. But how is it done? using requires the resource to implement the disposable protocol. An object is disposable if it has the [Symbol.dispose]() method. This method is called with no arguments to perform cleanup. For example, in the reader case, the [Symbol.dispose] property can be a simple alias or wrapper of releaseLock:

// For demonstration
class MyReader {
  // A wrapper
  [Symbol.dispose]() {
    this.releaseLock();
  }
  releaseLock() {
    // Logic to release resources
  }
}

// OR, an alias
MyReader.prototype[Symbol.dispose] = MyReader.prototype.releaseLock;

Through the disposable protocol, using can dispose all resources in a consistent fashion without understanding what type of resource it is.

Every scope has a list of resources associated with it, in the order they were declared. When the scope exits, the resources are disposed in reverse order, by calling their [Symbol.dispose]() method. For example, in the example above, reader1 is declared before reader2, so reader2 is disposed first, then reader1. Errors thrown when attempting to dispose one resource will not prevent disposal of other resources. This is consistent with the try...finally pattern, and respects possible dependencies between resources.

await using is very similar to using. The syntax tells you that an await happens somewhere—not when the resource is declared, but actually when it's disposed. await using requires the resource to be async disposable, which means it has an [Symbol.asyncDisposable]() method. This method is called with no arguments and returns a promise that resolves when the cleanup is done. This is useful when the cleanup is asynchronous, such as fileHandle.close(), in which case the result of the disposal can only be known asynchronously.

{
  await using fileHandle = open("file.txt", "w");
  await fileHandle.write("Hello");

  // fileHandle.close() is called and awaited
}

Because await using requires doing an await, it is only permitted in contexts where await is, which includes inside async functions and top-level await in modules.

Resources are cleaned up sequentially, not concurrently: the return value of one resource's [Symbol.asyncDispose]() method will be awaited before the next resource's [Symbol.asyncDispose]() method is called.

Some things to note:

The DisposableStack and AsyncDisposableStack objects

using and await using are special syntaxes. Syntaxes are convenient and hide a lot of the complexity, but sometimes you need to do things manually.

For one common example: what if you don't want to dispose the resource at the end of this scope, but at some later scope? Consider this:

let reader;
if (someCondition) {
  reader = stream.getReader();
} else {
  reader = stream.getReader({ mode: "byob" });
}

As we said, using is like const: it must be initialized and can't be reassigned, so you may attempt this:

if (someCondition) {
  using reader = stream.getReader();
} else {
  using reader = stream.getReader({ mode: "byob" });
}

However, this means all logic has to be written inside the if or else, causing a lot of duplication. What we want to do is to acquire and register the resource in one scope but dispose it in another. We can use a DisposableStack for this purpose, which is an object which holds a collection of disposable resources and which itself is disposable:

{
  using disposer = new DisposableStack();
  let reader;
  if (someCondition) {
    reader = disposer.use(stream.getReader());
  } else {
    reader = disposer.use(stream.getReader({ mode: "byob" }));
  }
  // Do something with reader
  // Before scope exit, disposer is disposed, which disposes reader
}

You may have a resource that does not yet implement the disposable protocol, so it will be rejected by using. In this case, you can use adopt().

{
  using disposer = new DisposableStack();
  // Suppose reader does not have the [Symbol.dispose]() method,
  // then it cannot be used with using.
  // However, we can manually pass a disposer function to disposer.adopt
  const reader = disposer.adopt(stream.getReader(), (reader) =>
    reader.releaseLock(),
  );
  // Do something with reader
  // Before scope exit, disposer is disposed, which disposes reader
}

You may have a disposal action to perform but it's not "tethered" to any resource in particular. Maybe you just want to log a message saying "All database connections closed" when there are multiple connections open simultaneously. In this case, you can use defer().

{
  using disposer = new DisposableStack();
  disposer.defer(() => console.log("All database connections closed"));
  const connection1 = disposer.use(openConnection());
  const connection2 = disposer.use(openConnection());
  // Do something with connection1 and connection2
  // Before scope exit, disposer is disposed, which first disposes connection1
  // and connection2 and then logs the message
}

You may want to do conditional disposal—for example, only dispose claimed resources when an error occurred. In this case, you can use move() to preserve the resources which would otherwise be disposed.

class MyResource {
  #resource1;
  #resource2;
  #disposables;
  constructor() {
    using disposer = new DisposableStack();
    this.#resource1 = disposer.use(getResource1());
    this.#resource2 = disposer.use(getResource2());
    // If we made it here, then there were no errors during construction and
    // we can safely move the disposables out of `disposer` and into `#disposables`.
    this.#disposables = disposer.move();
    // If construction failed, then `disposer` would be disposed before reaching
    // the line above, disposing `#resource1` and `#resource2`.
  }
  [Symbol.dispose]() {
    this.#disposables.dispose(); // Dispose `#resource2` and `#resource1`.
  }
}

AsyncDisposableStack is like DisposableStack, but for use with async disposable resources. Its use() method expects an async disposable, its adopt() method expects an async cleanup function, and its dispose() method expects an async callback. It provides a [Symbol.asyncDispose]() method. You can still pass it sync resources if you have a mix of both sync and async.

The reference for DisposableStack contains more examples and details.

Error handling

A major use case of the resource management feature is to ensure that resources are always disposed, even when an error occurs. Let us investigate some complex error handling scenarios.

We start with the following code, which, by using using, is robust against errors:

async function readUntil(stream, text) {
  // Use `using` instead of `await using` because `releaseLock` is synchronous
  using reader = stream.getReader();
  let chunk = await reader.read();

  while (!chunk.done && chunk.value !== text) {
    console.log(chunk.toUpperCase());
    chunk = await reader.read();
  }
}

Suppose that chunk turns out to be null. Then toUpperCase() will throw a TypeError, causing the function to terminate. Before the function exits, stream[Symbol.dispose]() is called, which releases the lock on the stream.

const stream = new ReadableStream({
  start(controller) {
    controller.enqueue("a");
    controller.enqueue(null);
    controller.enqueue("b");
    controller.enqueue("c");
    controller.close();
  },
});

readUntil(stream, "b")
  .catch((e) => console.error(e)) // TypeError: chunk.toUpperCase is not a function
  .then(() => {
    const anotherReader = stream.getReader();
    // Successfully creates another reader
  });

So, using does not swallow any errors: all errors that occur are still thrown, but the resources get closed right before that. Now, what happens if the resource cleanup itself also throws an error? Let's use a more contrived example:

class MyReader {
  [Symbol.dispose]() {
    throw new Error("Failed to release lock");
  }
}

function doSomething() {
  using reader = new MyReader();
  throw new Error("Failed to read");
}

try {
  doSomething();
} catch (e) {
  console.error(e); // SuppressedError: An error was suppressed during disposal
}

There are two errors generated in the doSomething() call: an error thrown during doSomething, and an error thrown during disposal of reader because of the first error. Both errors are thrown together, so what you caught is a SuppressedError. This is a special error that wraps two errors: the error property contains the later error, and the suppressed property contains the earlier error, which gets "suppressed" by the later error.

If we have more than one resource, and both of them throw an error during disposal (this should be exceedingly rare–it's already rare for disposal to fail!), then each earlier error is suppressed by the later error, forming a chain of suppressed errors.

class MyReader {
  [Symbol.dispose]() {
    throw new Error("Failed to release lock on reader");
  }
}

class MyWriter {
  [Symbol.dispose]() {
    throw new Error("Failed to release lock on writer");
  }
}

function doSomething() {
  using reader = new MyReader();
  using writer = new MyWriter();
  throw new Error("Failed to read");
}

try {
  doSomething();
} catch (e) {
  console.error(e); // SuppressedError: An error was suppressed during disposal
  console.error(e.suppressed); // SuppressedError: An error was suppressed during disposal
  console.error(e.error); // Error: Failed to release lock on reader
  console.error(e.suppressed.suppressed); // Error: Failed to read
  console.error(e.suppressed.error); // Error: Failed to release lock on writer
}
Examples Automatically releasing object URLs

In the following example, we create an object URL to a blob (in a real application, this blob would be fetched from somewhere, such as a file or a fetch response) so we can download the blob as a file. In order to prevent a resource leak, we must free the object URL with URL.revokeObjectURL() when it is no longer needed (that is, when the download has successfully started). Because the URL itself is just a string and therefore doesn't implement the disposable protocol, we cannot directly declare url with using; therefore, we create a DisposableStack to serve as the disposer for url. The object URL is revoked as soon as disposer goes out of scope, which is when either link.click() finishes or an error occurs somewhere.

const downloadButton = document.getElementById("download-button");
const exampleBlob = new Blob(["example data"]);

downloadButton.addEventListener("click", () => {
  using disposer = new DisposableStack();
  const link = document.createElement("a");
  const url = disposer.adopt(
    URL.createObjectURL(exampleBlob),
    URL.revokeObjectURL,
  );

  link.href = url;
  link.download = "example.txt";
  link.click();
});
Automatically cancelling in-progress requests

In the following example, we fetch a list of resources concurrently using Promise.all(). Promise.all() fails and rejects the resulting promise as soon as one request failed; however, the other pending requests continue to run, despite their results being inaccessible to the program. To avoid these remaining requests needlessly consuming resources, we need to automatically cancel in-progress requests whenever Promise.all() settles. We implement cancellation with an AbortController, and pass its signal to every fetch() call. If Promise.all() fulfills, then the function returns normally and the controller aborts, which is harmless because there's no pending request to cancel; if Promise.all() rejects and the function throws, then the controller aborts and cancels all pending requests.

async function getAllData(urls) {
  using disposer = new DisposableStack();
  const { signal } = disposer.adopt(new AbortController(), (controller) =>
    controller.abort(),
  );

  // Fetch all URLs in parallel
  // Automatically cancel any incomplete requests if any request fails
  const pages = await Promise.all(
    urls.map((url) =>
      fetch(url, { signal }).then((response) => {
        if (!response.ok)
          throw new Error(
            `Response error: ${response.status} - ${response.statusText}`,
          );
        return response.text();
      }),
    ),
  );
  return pages;
}
Pitfalls

The resource disposal syntax offers a lot of strong error handling guarantees that ensure the resources are always cleaned up no matter what happens, but there are some pitfalls you may still encounter:

The resource management feature is not a silver bullet. It is definitely an improvement over manually invoking the disposal methods, but it is not smart enough to prevent all resource management bugs. You still need to be careful and understand the semantics of the resources you are using.

Conclusion

Here are the key components of the resource management system:

With proper usage of these APIs, you can create systems interacting with external resources that remain strong and robust against all error conditions without lots of boilerplate code.


RetroSearch is an open source project built by @garambo | Open a GitHub Issue

Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo

HTML: 3.2 | Encoding: UTF-8 | Version: 0.7.4