Home > Software engineering >  How to properly await createWriteStream?
How to properly await createWriteStream?

Time:12-06

Async programming in node.js is usually done with callbacks. I find callback-based code hard to read and reason about, which is why I'm using async & await whenever I can. This almost always works well and leads to robust code. However, rarely I'm wondering whether I'm making things more difficult than necessary. For example, how do you create a stream such that you can await its creation? More specifically, the result of the await should of course be the stream itself when things go well. When they don't, an appropriate exception should be thrown.

The best I could come up with is the function below. It feels very clunky and I'm wondering whether there is an easier way to do this?

import type { EventEmitter } from "events";

export const createStreamAsync = async <T extends EventEmitter>(create: () => T): Promise<T> => {
    const result = create();
    let onOpen = (): void => void 0;
    let one rror = (reason?: unknown): void => void reason;

    try {
        await new Promise<void>((resolve, reject) => {
            onOpen = resolve;
            one rror = reject;
            result.on("open", onOpen).on("error", one rror);
        });

        return result;
    } finally {
        result.removeListener("open", onOpen);
        result.removeListener("error", one rror);
    }
};

You'd use the function as follows:

import { createWriteStream } from "fs";
import { createStreamAsync } from "./createStreamAsync.js";

try {
    const stream = await createStreamAsync(() => createWriteStream("~/whatever.txt"));
    // Use stream ...
} catch (error: unknown) {
    // Handle error
}

CodePudding user response:

Readable streams have an AsyncIterator symbol, so they can be processed with for await ... of:

const readable = fs.createReadStream('file.txt');
for await (const chunk of readable) {
  console.log(chunk);
}

You can listen to both an event and catch the error event (if any) with events.once:

const { once } = require('node:events');
const writable = fs.createWriteStream('file.txt');
try {
  await once(writable, 'open');
  // If the event emits data, `once` returns it as an array: 
  // const x = await once(...);
} catch (err) {
  console.error('Could not open file', err);
}

This will automatically remove the listener, just like EventEmitter.once does.

event.on returns an AsyncIterator to handle multiple events:

const { on } = require('node:events');
const { exec } = require('node:child_process');
const child = exec(...);
try {
  for await (const event of on(child, 'message')) {
    // `event` is an array of 0 or more values.
    console.log(event);
  }
} catch (err) {
  console.error(err);
}
  • Related