Skip to content

add promises API#171

Merged
thejoshwolfe merged 3 commits into
masterfrom
promises
Jun 7, 2026
Merged

add promises API#171
thejoshwolfe merged 3 commits into
masterfrom
promises

Conversation

@thejoshwolfe

@thejoshwolfe thejoshwolfe commented May 24, 2026

Copy link
Copy Markdown
Owner

yauzl roars into the year 2017 with the addition of promises and proper async/await support!

  • Add wrapper functions that give Promise semantics: openPromise(), fromFdPromise(), fromBufferPromise(), fromRandomAccessReaderPromise(), openReadStreamPromise(), readLocalFileHeaderPromise(), openReadStreamLowLevelPromise().
  • Add eachEntry(), which gives async iterator semantics as an alternative to readEntry(), Event: "entry", Event: "end", and Event: "error".
  • Added more modern examples to README.md and examples/ using await on the new APIs.

This is the new recommended usage at the top of the readme:

const yauzl = require("yauzl");

(async () => {
  try {
    const zipfile = await yauzl.openPromise("path/to/file.zip");
    for await (let entry of zipfile.eachEntry()) {
      if (entry.fileName.endsWith("/")) {
        // Directory file names end with '/'.
        // Note that entries for directories themselves are optional.
        // An entry's fileName implicitly requires its parent directories to exist.
        continue;
      } else {
        // file entry
        const readStream = await zipfile.openReadStreamPromise(entry);
        await stream.promises.pipeline(readStream, somewhere);
      }
    }
  } catch (err) {
    // Indicates a malformed zipfile or I/O error.
    throw err;
  }
})();

All the old APIs work the same. This PR only adds new alternative ways of calling things.

Why now? Why not earlier? Why at all?

My intent with spending so much effort over the years polishing yauzl to be as high quality as it's been, is that yauzl can be "done" kinda, as far as node.js software goes. I can be pretty confident that this library is as useful now as it was 10 years ago, and will be for the next 10 years. Adding first-class integration with fancy new language features is not necessary, because this is a solved problem. Only if the old way of doing things breaks out from under me do I have to do anything, such as with the destroy() method, or the deprecated buffer constructor.

So that's why I never bothered to add promise semantics before this. I stopped writing any significant amount of node.js software myself years ago, so I'm not the one demanding the feature. And if nothing's broken, then there's nothing to fix.

However, while debugging #169, I was kinda forced to play around with async/await, promises, async iterators, etc., and I was impressed by just how easy it all was. After I wrote the code to repro the issue, I figured I might as well integrate the code into the core library and properly support it forever.

And yeah, who'd have thought. The modern await style usage of the API is actually much nicer than the legacy callbacks style. It's terser for sure, and I'd bet it's harder to get wrong, although that's really up to the client authors.

This does complicate the API surface area of yauzl and thereby the testing burden, which is evident from all the warnings around mixing readEntry() and eachEntry(). But it seems worth it.

@thejoshwolfe thejoshwolfe marked this pull request as ready for review May 24, 2026 23:10
@Moriarty47

Copy link
Copy Markdown

I suggest replacing the trailing Promise with Async (e.g., openAsync). What do you think?

@thejoshwolfe

Copy link
Copy Markdown
Owner Author

@Moriarty47 thanks for the suggestion! Is this recommendation coming from a precedent somewhere? I see the parallels with node's fs.readFileSync(). is that what you're thinking of, or is there an even better example of an API with doSomethingAsync()?

In contrast to fs.readFileSync(), one might assume the non-async suffixed version might be blocking, which is certainly not the case. But it might be more intuitive to assume that the *Async implementation is an async function, which is very close to correct.

@Moriarty47

Moriarty47 commented May 31, 2026

Copy link
Copy Markdown

@thejoshwolfe Hmm... you're right. For Node, asynchronous non-blocking behavior is the primary concern, so fs.readFile is asynchronous by default. But since you're adding onto existing APIs, your certainly wouldn't want to make breaking changes, and using *Async seems like a compromise, even if it somewhat runs counter to Node's naming convention.

Alternatively, you could follow the node:fs/promises approach, expose another entry and keep the same API names, that also seems intuitive.

@thejoshwolfe

Copy link
Copy Markdown
Owner Author

Alternatively, you could follow the node:fs/promises approach, expose another entry and keep the same API names, that also seems intuitive.

I'm glad to hear someone's opinion on that design. I find it baffling that you have the same names of functions with different semantics. Baffling doesn't mean bad, and I might just be out of the loop with modern JS programming idioms. I suppose typescript linters will help you not confuse the two?

I definitely don't want to have two APIs where the only difference is the control flow paradigm. That's just too hard for me to stomach.

In terms of the actual semantics of the two paradigms:

  1. old style is to pass a callback parameter.
  2. new style is to return a Promise.

If we were designing both at the same time, it'd probably be best to make the Promise version the default name, and call the callback parameter version something unusual. (Or rather just don't even make the callback version, because you can transform a Promise into the old style of callbacks pretty easily.) But since we have to name the Promise version something different...

I believe the term Promise is the most correct description of this new style. Some other aspects of the modern style are:

  • async function - not quite right, because it's not implemented with the async keyword, but the way you call the API is the same as for async functions.
  • "thenable" - this is correct, but less precise than Promise, because promises have more APIs on them than just then().
  • "awaitable" - also correct, but less precise, because there are other ways to use promises than to directly await them.

My project is far too small to try to establish naming conventions (for a 9 year old JavaScript paradigm, lmao). But the term Promise has a lot going for it, given that it is exactly correct. I'm very happy to hear other arguments though.

@Moriarty47

Copy link
Copy Markdown

Thanks for your time and the detailed discussion. Nearly 4 million downloads per week is not a “small project.” : )

And sorry, I still don’t fully get it:

async function - not quite right, because it's not implemented with the async keyword, but the way you call the API is the same as for async functions.

async is just syntactic sugar for Promise, a function declared with the async keyword actually returns a Promise. You only use async when you want to use await inside the function or when you want to make the return value a Promise without writing a plain Promise.

...As I write this I think I understand your point: you named it *Promise mainly because the function is implemented with Promise, right?

After looking at your changed code, I found many implementations like this.

function openPromise(path, options) {
  return new Promise((resolve, reject) => {
    open(path, {...options, lazyEntries: true}, function(err, zipfile) {
      if (err) return reject(err);
      resolve(zipfile);
    });
  });
}

I think it can be done directly with built-in functions util.promisify.

const { promisify } = require('node:util');
const promisifiedOpen = promisify(open);
function openPromise(path, options) {
  return promisifiedOpen(path, {...options, lazyEntries: true});
}

Thanks again for your time and effort.

@thejoshwolfe

Copy link
Copy Markdown
Owner Author

async is just syntactic sugar for Promise

Right. Since i'm not using await, i'm not using the async keyword either.

you named it *Promise mainly because the function is implemented with Promise, right?

Right. The function returns a Promise.

I think it can be done directly with built-in functions util.promisify.

I am aware of the promisification facilities availble in node, but it's unnecessary complexity. The docs on that function include 5 paragraphs of discussion and links/references 2 more sections with 6 more paragraphs. Here is the source code for just the promisify function itself: https://github.com/nodejs/node/blob/v26.3.0/lib/internal/util.js#L455-L514 , which calls another 6 different functions by my count, that i did not recursively analyze.

All that's unnecessary complexity compared against a cool 8 lines of code per promisification in my own code. Also, some users have expressed interest in porting yauzl to non-node environments (like the browser), and i have to imagine that avoiding node-specific builtins makes that easier, but i haven't tried going through the process myself, so this advantage is speculative.

@Moriarty47

Copy link
Copy Markdown

Okay, got it. You're right, I think I fell into the trap of over-engineering. Keep it simple, reduce unnecessary complexity. simple, readable, and easy-to-change code is the correct practice.

I'm glad we talked so much, and I learned a lot. Thank you so much for your time and patient answers.

@thejoshwolfe thejoshwolfe merged commit 632d650 into master Jun 7, 2026
7 checks passed
@thejoshwolfe thejoshwolfe deleted the promises branch June 7, 2026 12:57
@XhmikosR

XhmikosR commented Jun 8, 2026

Copy link
Copy Markdown

Thanks for this, this really simplifies and improves the API ❤️

For example, XhmikosR/decompress-unzip#2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants