Search code examples
javascriptnode.jsasynchronousasync-awaithttp-post

Can't understand the execution flow in this asynchronous sample


I'm learning javascript and nodejs and still strugling with the asynchronous execution

I know the following code is far from best practice, but I need to understand why, depending on whether or not fileHandle.write('[1]') is called, the file will still be opened or not when fileHandle.write('[2]') is called.

I found this in the nodejs documentation, I guess it's related, but I'd like to understand how things work: "It is unsafe to use filehandle.write() multiple times on the same file without waiting for the promise to be resolved (or rejected). For this scenario, use filehandle.createWriteStream()."

Code without error:

import {createServer} from 'node:http'
import {open} from 'node:fs/promises'
import {write} from 'node:fs'
import {json} from 'node:stream/consumers'


const serveur = createServer(async (request,response) => {
    const path = './tp_CRUD/storage/my_file.json'
    const fileHandle = await open(path, 'w+')
    try {
        await fileHandle.read()
        .then(() => {
            fileHandle.write('[1]') // <= no error as the file is still opened at this point

            json(request) // promise parsing request content to json
            .then(() => {
                fileHandle.write('[2]') // <= no error as file is still open somehow thanks to [1] line
            })
        })
    } catch {
        console.log('error with POST request')
    } finally {
        fileHandle.close()
    }
    response.end()

})
serveur.listen('3000')

Code with error:

import {createServer} from 'node:http'
import {open} from 'node:fs/promises'
import {write} from 'node:fs'
import {json} from 'node:stream/consumers'


const serveur = createServer(async (request,response) => {
    const path = './tp_CRUD/storage/my_file.json'
    const fileHandle = await open(path, 'w+')
    try {
        await fileHandle.read()
        .then(() => {
            json(request) // promise parsing request content to json
            .then(() => {
                fileHandle.write('[2]') // <= error as file is closed despite being in the try{} block ??
            })
        })
    } catch {
        console.log('error with POST request')
    } finally {
        fileHandle.close()
    }
    response.end()

})
serveur.listen('3000')

here is the callstack if it can help somehow

node:internal/fs/promises:436
    const err = new Error('file closed');
                ^

Error: file closed
    at fsCall (node:internal/fs/promises:436:17)
    at FileHandle.write (node:internal/fs/promises:207:12)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  code: 'EBADF',
  syscall: 'write'
}

Node.js v21.1.0

Solution

  • The way you have the calls laid starts two async chains that lead to a race condition as there's no guarantee the write call happens before the close call happens.

    When you have

    await fileHandle.read()
            .then(() => {
                // A
                json(request)
                .then(() => {
                    // B
                    fileHandle.write('[2]') // <= error as file is closed despite being in the try{} block ??
                })
    

    You are queueing a new async task B with no relation to A and execution may proceed before B runs. If you want to chain them together you have to have the then() function return a Promise to append to the chain.

    If you want to continue using the then() notation you need to chain them together like so:

    await fileHandle.read()
            .then((fileData) => json(request) )
            .then((jsonData) => fileHandle.write('[2]') )
    

    Or like so

    await filehandle.read()
      .then((fileData) => {
        // do some stuff
        return json(request);
      }).then( (jsonData) => fileHandle.write('foobar') )
    

    This way the await call will wait for the entire chain.

    Alternatively, you could lose the then() notations altogether and just use awaits like so:

    let fileData = await fileHandle.read()
    let jsonData = await json(request) 
    await fileHandle.write('[2]')