Skip to main content

Command Palette

Search for a command to run...

My CLI was slow — then I stopped awaiting everything

Published
2 min read
My CLI was slow — then I stopped awaiting everything
L
I write about things that interest me and things i have learned

I build a CLI that scans a directory full of projects and runs npm audit --json on each one to find vulnerabilities. It worked but it felt slow. Every project waited for the previous one to finish before starting.

I was doing something like this (roughly):

for (const dir of dirs) { 
    await runAudit(dir)
}

The problem is each npm audit call was waiting for the one before it to finish. All of them could run at the same time, but I was forcing them to run one by one.

The fix Promise.all

Instead of awaiting each call inside the loop, I stored all the promises into an array and resolved them all at once:

const dirs = await fs.readdir(config.path);

for (let dir of dirs) {
  let dirFullPath = `\({config.path}\){dir}`;
  const projectDir = await fs.readdir(dirFullPath);
  if (projectDir.includes("package.json")) {
    projects.push(getAuditPromise(dirFullPath, dir, spinner));
  }
}

projectsOutputs = await Promise.all(projects);

Now all the npm audit calls kick off at the same time. Way faster, way smoother.

The thing I learned: await inside a loop is usually bad. If the tasks don't depend on each other.

But then I learned about Promise.allSettled

After getting this working I realized there's a problem. If one project has a corrupted package.json and npm audit throws, Promise.all cancels everything. You lose results from all the other projects.

Promise.allSettled waits for every promise to finish. Each result tells you whether it succeeded or failed:

const results = await Promise.allSettled(projects);

for (const result of results) {
  if (result.status === "fulfilled") {
    projectAudits.push(result.value);
  } else {
    spinner.warn(result.reason);
  }
}

One thing that caught me out. Your promises need to actually reject on failure, otherwise everything shows as fulfilled even when something went wrong. I had my function resolving bad output and allSettled had nothing to catch. Took me a minute to figure out why it wasn't working.

For a vulnerability scanner this is probably more correct. If a project fails I can still show the user which ones succeeded and warn them that a specific project might have a corrupted lock file.

Thats it

Have a great Day :)