0

I haven't found anything specific about this, it isn't really a problem but I would like to understand better what is going on here.

Basically, I'am testing some simple NodeJS code , like this :

//Summary : Open a file , write to the file, delete the file.

let fs = require('fs');

fs.open('mynewfile.txt' , 'w' , function(err,file){
    if(err) throw err;
    console.log('Created file!')
})

fs.appendFile('mynewfile.txt' , 'Depois de ter criado este ficheiro com o fs.open, acrescentei-lhe data com o fs.appendFile' , function(err){
    if (err) throw err;
    console.log('Added text to the file.')
})

fs.unlink('mynewfile.txt', function(err){
    if (err) throw err;
        console.log('File deleted!')
})

console.log(__dirname);

I thought this code would be executed in the order it was written from the top to the bottom, but when I look at the terminal I'am not sure that was the case because this is what I get :

$ node FileSystem.js 
C:\Users\Simon\OneDrive\Desktop\Portfolio\Learning Projects\NodeJS_Tutorial
Created file!
File deleted!
Added text to the file.

//Expected order would be: Create file, add text to file , delete file , log dirname.

Instead of what ther terminal might make you think, in the end when I look at my folder the code order still seems to have been followed somehow because the file was deleted and I have nothing left on the directory.

So , I was wondering , why is it that the terminal doesn't log in the same order that the code is written from the top to the bottom. Would this be the result of NodeJS asynchronous nature or is it something else ?

izzypt
  • 170
  • 2
  • 16
  • 1
    Basically, yes. You either need to use `then` or `async await` to ensure right order. If your usecase is very basic, you may switch to the [sync functions](https://stackoverflow.com/questions/21611165/why-does-node-js-have-both-async-sync-version-of-fs-methods) or to a `fs-sync` library instead. – k0pernikus Mar 26 '21 at 13:57

3 Answers3

3

The code is (in princliple) executed from top to bottom, as you say. But fs.open, fs.appendFile, and fs.unlink are asynchronous. Ie, they are placed on the execution stack in the partiticular order, but there is no guarantee whatsoever, in which order they are finished, and thus you can't guarantee, in which order the callbacks are executed. If you run the code multiple times, there is a good chance, that you may encounter different execution orders ...

If you need a specific order, you have two different options

  1. You call the later operation only in the callback of the prior, ie something like below

    fs.open('mynewfile.txt' , 'w' , function(err,file){
      if(err) throw err;
      console.log('Created file!')
    
      fs.appendFile('mynewfile.txt' , '...' , function(err){
        if (err) throw err;
        console.log('Added text to the file.')
    
        fs.unlink('mynewfile.txt', function(err){
          if (err) throw err;
          console.log('File deleted!')
        })
      })
    })
    

    You see, that code gets quite ugly and hard to read with all that increasing nesting ...

  2. You switch to the promised based approach

    let fs = require('fs').promises;
    
    fs.open("myfile.txt", "w")
      .then(file=> {
        return fs.appendFile("myfile.txt", "...");
      })
      .then(res => {
        return fs.unlink("myfile");
      })
      .catch(e => {
         console.log(e);
      })
    

    With the promise-version of the operations, you can also use async/await

    async function doit() {
       let file = await fs.open('myfile.txt', 'w');
       await fs.appendFile('myfile.txt', '...');
       await fs.unlink('myfile.txt', '...');
    }
    

For all three possibilites, you probably need to close the file, before you can unlink it.

For more details please read about Promises, async/await and the Execution Stack in Javascript

derpirscher
  • 14,418
  • 3
  • 18
  • 35
2

It's a combination of 2 things:

  • The asynchronous nature of Node.js, as you correctly assume
  • Being able to unlink an open file

What likely happened is this:

  • The file was opened and created at the same time (open with flag w)
  • The file was opened a second time for appending (fs.appendFile)
  • The file was unlinked
  • Data was appended to the file (while it was already unlinked) and the file was closed

When data was being appended, the file still existed on disk as an inode, but had zero hard links (references) to it. It still takes up space then, but the OS checks the reference count when closing and frees up the space if the count has fallen to zero.

People sometimes run into a similar situation with daemons such as HTTP servers that employ log rotation: if something goes wrong when switching over logs, the old log file may be unlinked but not closed, so it's never cleaned up and it takes space forever (until you reboot or restart the process).

Note that the ordering of operations that you're observing is random, and it is possible that they would be re-ordered. Don't rely on it.

Robert Kawecki
  • 2,218
  • 1
  • 9
  • 17
0

You could write this as (untested):

let fs = require('fs');

const main = async () => {
  await fs.open('mynewfile.txt' , 'w');
  await fs.appendFile('mynewfile.txt' , 'content');
  await fs.unlink('mynewfile.txt');
});

main()
  .then(() => console.log('success'()
  .catch(console.error);

or within another async function:

const someOtherFn = async () => {
   try{
      await main();
   } catch(e) {
     // handle any rejection to your liking
   }
}

(The catch block is not mandatory. You can opt to just let them throw to the top. It's just to showcase how async / await allows you to make synchronous code appear as if it was synchronous code without runing into callback hell.)

k0pernikus
  • 60,309
  • 67
  • 216
  • 347
  • I think by `require('fs')` you still get the callback form of the fs-API. So `await fs.open()` as you use it will throw an error about the missing callback – derpirscher Mar 26 '21 at 14:18