Register to get access to free programming courses with interactive exercises

Asynchronous code JS: Asynchronous programming

The examples in this lesson are based on the file system because they best capture the essence of asynchronous programming. The principles of asynchronous code are totally identical for both frontend and backend.

In synchronous code, functions are executed in the same place where they're called and at the moment they're called. It's different in asynchronous code. Calling a function doesn't mean that it'll run there and then. Moreover, we don't know when it will work work. Let's look at an example where a file is copied by reading and writing it again to another file:

import fs from 'fs';

// Be sure to pass the second parameter `utf-8`,
// only then will the data be returned in string form
const content = fs.readFileSync('./myfile', 'utf-8');
fs.writeFileSync('./myfile-copy', content);

Example code written for Node.js: it uses the fs module and its synchronous functions to read and write the file (their peculiarity is that the names end with Sync). This code works as expected: it first reads the contents of a file into the constant content and then writes them to another file. Each line causes a block, i.e., the program doesn't execute until the operating system has read the file (and it's the OS that needs to do it) and gives its contents to the program. Only then is the next line executed. Accordingly, when writing takes place, the program waits for the operating system to write the file to disk (this isn't wholly true, but this topic is beyond the scope of the lesson, more details can be found in books about operating systems), and only then does it proceed.

Sync-io

In principle, we could have stopped here – why do anything else with this code? The point is that any file operation will take a long time (they are thousands of times slower than calling a normal function). During this time, the process waits for a response from the kernel about the result of the operation without doing anything else. Therefore, the synchronous approach when it comes to working with file systems is very inefficient in utilizing resources. Asynchronous code, on the other hand, continues to run during all file operations. In other words, the code is never blocked on IO operations, but it can learn about them having been completed. Properly written asynchronous programs (in situations where it's needed) are much more efficient than synchronous programs. Sometimes, this is so critical that the synchronous version simply can't do the job.

Note: IO means input/output. This includes not only file handling, but also all networking (which, in the end, comes down to file handling). Even printing to the screen is also writing to a file.

Now make our way to asynchronous code trying to understand how it works. Let's assume that the readFile function in the example below is asynchronous. This means that it reads the file not right where it was called, but somewhere else in the background:

import fs from 'fs';

// empty function, we'll analyze its meaning a bit later,
// but the asynchronous version of readFile requires a third parameter to be passed to the function
const noop = () => {};
const content = fs.readFile('./myfile', 'utf-8', noop);
console.log(content);

Is this asynchronous code possible? The answer is no. It doesn't matter what specific task this function performs, the only important thing is that it doesn't perform it immediately, which means that it isn't possible to return the result of an asynchronous operation. If we run similar code, we can see the following output:

node index.js
undefined

This is a fundamental feature of asynchronous functions that really, absolutely, must be remembered. I have to emphasize this point because newcomers constantly stumble on it, trying to work with asynchronous functions in the same way as with synchronous ones.

Then the this question arises: How do you get the result of this function? And for this, we use another function, which is passed to an asynchronous function. As such, it's called a callback. This function will be called when the operation is over (possibly with an error). It has the following signature: callback(error, result). The first parameter, error, is passed if things don't go well, the second, the result of the operation, is passed if everything was OK. We'll discuss errors later, but now let's review the general principles of work.

import fs from 'fs';

// Called when an asynchronous operation is executed
const callback = (_error, data) => console.log(data);
// the lower underscore prefix indicates an unused argument

// readFile starts the file reading task.
// Be sure to pass utf-8 as the second parameter Only then will the data be read in string form.
fs.readFile('./myfile', 'utf-8', callback);

Running and output:

node index.js
content of file

As soon as the file reading operation was finished, the Node.js interpreter internally called a callback by passing the contents of the file to it as a parameter. It remains to be seen if this code really is asynchronous:

import fs from 'fs';

const callback = (_error, data) => console.log(data);
console.log('before read');
// the function call doesn't wait for the file to finish being read, the code immediately continues to be executed
fs.readFile('./myfile', 'utf-8', callback);
console.log('after read?');

Running and output:

node index.js

before read
after read?
content of file

Although after read? is output by the last instruction, the actual output differs from the order of instructions in the code. Although the asynchronous function starts to execute immediately, the callback is only called when there are no functions left in the current call stack. In our case, this means that the callback runs only after the entire file has been processed. And this run will spawn its own call stack.

In asynchronous code, each callback of an asynchronous function generates its own call stack, which, in turn, can make new asynchronous calls and so on and so forth ad infinitum.

Node.js waits until all of the asynchronous calls that were made while the program was running are completed:

import fs from 'fs';

fs.readFile('./myfile', 'utf-8', (_error, data) => console.log('First!'));
fs.readFile('./myfile', 'utf-8', (_error, data) => console.log('Second!'));

In the example above, we can see two asynchronous operations running. We now know that the second reading of the file will run almost simultaneously with the first, since the operations are asynchronous and their execution does not block the execution of the program. Try to answer this question: In what order will the results appear?

Running and output:

node index.js

Second!
First!

node index.js

First!
Second!

As you can see, this question cannot be answered definitively. Asynchronous operations can run in any order as long as they run simultaneously. And the only way to organize them is to run them sequentially; we'll talk about that later.

Asynchronous programming is much more complicated than synchronous programming. Seeing linear code (sequentially when written) and thinking about it non-linearly is very hard. Later, you'll see that as the number of asynchronous functions within one program increases exponentially, you'll at some point you stop understanding what's going on. Many ways have been devised to combat it, some of which have proved very successful and will be discussed in later lessons in the course.


Recommended materials

  1. synchronous readFileSync function from fs module
  2. asynchronous readFile function from fs module

Hexlet Experts

Are there any more questions? Ask them in the Discussion section.

The Hexlet support team or other students will answer you.

About Hexlet learning process

For full access to the course you need a professional subscription.

A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.

Get access
130
courses
1000
exercises
2000+
hours of theory
3200
tests

Sign up

Programming courses for beginners and experienced developers. Start training for free

  • 130 courses, 2000+ hours of theory
  • 1000 practical tasks in a browser
  • 360 000 students
By sending this form, you agree to our Personal Policy and Service Conditions

Our graduates work in companies:

<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.bookmate">Bookmate</span>
<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.healthsamurai">Healthsamurai</span>
<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.dualboot">Dualboot</span>
<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.abbyy">Abbyy</span>
Suggested learning programs

From a novice to a developer. Get a job or your money back!

Frontend Developer icon
Profession
beginner
Development of front-end components for web applications
start anytime 10 months

Use Hexlet to the fullest extent!

  • Ask questions about the lesson
  • Test your knowledge in quizzes
  • Practice in your browser
  • Track your progress

Sign up or sign in

By sending this form, you agree to our Personal Policy and Service Conditions
Toto Image

Ask questions if you want to discuss a theory or an exercise. Hexlet Support Team and experienced community members can help find answers and solve a problem.