If you have ever picked kids up from school (or been picked up), then you can understand the key concept behind Node.js: asynchronous i/o.
Have you ever learned a C-based language or Java? Before web development took off in the mid-2000s, those languages were the most popular. And they are still used by hundreds of thousands of companies today.
But, they are not well-suited for many types of web apps. Frameworks like Node.js and Ruby on Rails fill this gap by focusing on the common technical challenges of building a web app.
If you learned a language that executes code “synchronously” in the past or if you have never written back-end code at all, it can be a little tough to follow Node.js.
In a “synchronous” language, the code executes in the order that it is written. But in Node.js, the code may not execute in the order that you expect. The good news is that this will be helpful as you build and scale a web app.
I realized that it is kind of like the difference between taking a school bus and getting picked up from school. So, here’s how to understand what is going on behind the scenes when you build your first Node app.
In order to understand this tutorial, you just need to know the difference between front-end and back-end.
What’s the difference between synchronous and asynchronous?
Imagine that you attend elementary school in the United States. After school, all the kids want to go to after-school activities like dancing lessons, drawing lessons or soccer practice. But, they need a way to get there. They could:
- Take a bus
- Get picked up from school
There are advantages and disadvantages to each approach. The school superintendent needs to choose one of the two methods to get kids to their destinations.
Similarly, your web server can communicate with other servers. But, you need to choose a server-side language that will help your users accomplish their goals.
You can pick:
Here’s an example of how a web server can work with other services to accomplish the user’s goals:
In the middle is your web server. It is constantly communicating with the front-end and other services, depending on user requests. It is up to you to choose a language that can handle those requests and deliver a speedy response.
The Advantages of Asynchronous I/O
Okay, let’s imagine that you want to use buses to help kids reach their after-school destinations. You can only afford to rent a few buses at a time. And, these buses can only be at one place at one time, so if they are dropping off kids, the remaining kids at the school will need to wait until they get back.
Input/output (I/O) processing determines the way that a server interacts with other servers or services, like user input or a database. The example above is kind of like synchronous I/O. The buses become available once they have finished their previous task.
If your school does not have many kids that want to go to these activities, this might work! Or, they all want to go to the same place, it also could work. But, if you have a lot of kids with varied interests, your bus system will not be able to serve them in a timely manner. They will be stuck with waiting.
If you are using asynchronous I/O (like in Node.js), however, it’s kind of like having every parent available to pick their kids up after school and drive them to their destination. Yes, this never happens in the real-world, but this is programming, so it is a heck of a lot easier.
Any time, a kid gets out of school, their parent would be ready to pick them up and whisk them away.
Here’s what that looks like from a programming perspective.
The kid’s transportation needs are like the user requests. A user request is triggered when the user takes a specific action when interacting with the website or mobile app. Once the requests come into your Node server, you can coordinate with other services without making the users wait.
An Explanation of Blocking vs. Non-Blocking
Okay, perhaps at this point, you are asking, “Why can’t we just have unlimited buses, and get the best of both worlds?”
This is where the concept of threads comes in handy. If your server-side code is written using synchronous I/O, that means that your server can handle multiple threads. Synchronous code is blocking– that means that the entire thread will be prevented from handling more tasks while it is trying to complete a single task.
This is why synchronous code is like using buses. Your school system can only afford to hire a certain number of buses. A server can only handle so many threads. If you exceed that number, you will need to pay for another server. Or, your users will need to wait until a thread is available. Let’s say your school system has 10 buses.
That means that your server can handle 10 processes at once. Here’s another diagram from this excellent YouTube video.
But, asynchronous I/O uses a single thread and is non-blocking. That means that when a server needs to communicate with a database or API, for example, it’s single thread will not be occupied so it will be able to handle another request. Similarly, a child does not need to wait for their parent’s car based on what is going on with the other kids and parents. All of them are independent.
Imagine if you had a web app that needed to handle tens or hundreds of users at the same time. A multi-thread, blocking approach might not scale in the way you want. But, a non-blocking approach should be able to handle all the relatively simple inputs from your users.
The Challenges of Asynchronous Programming
There are plenty of cases where a synchronous approach will be a better solution. If you are building a program that needs significant computational power, the multiple threads can be helpful.
But, the biggest challenge for new developers (and old developers) is reading and reviewing the code. In a synchronous program, everything is in order. You can start on line one and understand the sequence of events.
But, in asynchronous I/O, you will need to spend more time finding the order of execution. Since the code likely will not execute in order due to requests and responses between services, you must follow individual callbacks to understand when certain code blocks are being executed.
A Quick Intro To Callbacks
We haven’t mentioned the dreaded concept of callbacks yet! We have just looked at synchronous vs. asynchronous at a high level. Callbacks allow you to write code that will execute asynchronously.
When your code executes in order ie. synchronously, you obviously don’t need to specify any type of sequence. It’s directly related to the line number.
But, in asynchronous code, you do need to specify the order. You need a way to specify what will happen next after a particular I/O process is completed, because the whole thread will be waiting on it.
This is where callbacks come in. They allow you to write functions based around the result of an asynchronous operation.
I wrote a whole separate guide to callbacks, but I can briefly cover them here. In our example above, imagine that we are using the asynchronous scenario where parents are picking up their kids in cars.
Rather than wait for an entire bus to return before continuing the code, each individual parent can do things in a custom order. Maybe one parent wants to:
- Drop child off at soccer
- Return to get next child
- Drop that child off at dance classes
Another parent wants to:
- Drop child off at soccer
- Get coffee
- Go to the library
Callbacks enable you to create these custom sequences based on the result of a previous operation. All of it can happen concurrently, since the main thread is not blocked like the bus scenario.
Get The Latest Tutorials