Computers are asynchronous
by design.
Asynchronous means that things can happen independently
of the main program flow.
In the current consumer computers, every program runs for a specific time slot and then it stops its execution to let another program continue their execution
. This thing runs in a cycle so fast that it's impossible to notice. We think our computers run many programs simultaneously, but this is an illusion (except on multiprocessor machines).
Programs internally use interrupts
, a signal that's emitted to the processor to gain the attention of the system.
I won't go into the internals of this, but just keep in mind that it's normal for programs to be asynchronous and halt their execution until they need attention, allowing the computer to execute other things in the meantime. When a program is waiting for a response from the network, it cannot halt the processor until the request finishes.
Normally, programming languages are synchronous
and some provide a way to manage asynchronicity
in the language or through libraries. C, Java, C#, PHP, Go, Ruby, Swift, and Python are all synchronous by default. Some of them handle async by using threads, spawning a new process.
JavaScript
JavaScript is synchronous
by default and is single threaded
. This means that code cannot create new threads and run in parallel.
Lines of code are executed in series, one after another, this type of execution flow is also called as Blocking Code/Mode
.
Blocking Code Example
Create a text file named input.txt with the following content -
No other lines will execute until the whole file is read.
Create a js file named app.js
with the following code -
var fs = require("fs");
var data = fs.readFileSync('input.txt');
console.log(data.toString());
console.log("Program Ended");
Now run the main.js
to see the result -
node main.js
Output:
No other lines will execute until the whole file is read.
Program Ended
Here, we can clearly see code of line are executed one after the other.
But JavaScript was born
inside the browser
, its main job, in the beginning, was to respond to user actions, like onClick
, onMouseOver
, onChange
, onSubmit
and so on. How could it do this with a synchronous programming model?
The answer was in its environment. The browser provides a way to do it by providing a set of APIs that can handle this kind of functionality.
More recently, Node.js introduced a non-blocking I/O environment
to extend this concept to file access, network calls and so on.
Callbacks
You can't know when a user is going to click a button. So, you define an event handler for the click event. This event handler accepts a function, which will be called when the event is triggered:
document.getElementById('button').addEventListener('click', () => {
//item clicked
})
This is the so-called callback
.
A callback is a simple function that's passed as a value to another function, and will only be executed when the event happens. We can do this because JavaScript has first-class functions, which can be assigned to variables and passed around to other functions (called higher-order functions)
It's common to wrap all your client code in a load event listener on the window object, which runs the callback function only when the page is ready:
window.addEventListener('load', () => {
//window loaded
//do what you want
})
Callbacks are used everywhere, not just in DOM events.
One common example is by using timers:
setTimeout(() => {
// runs after 2 seconds
}, 2000)
Handling errors in callbacks
How do you handle errors with callbacks? One very common strategy is to use what Node.js adopted: the first parameter in any callback function is the error object: error-first callbacks
If there is no error, the object is null
. If there is an error, it contains some description of the error and other information.
fs.readFile('/file.json', (err, data) => {
if (err !== null) {
//handle error
console.log(err)
return
}
//no errors, process data
console.log(data)
})
The problem with callbacks
Callbacks
are great for simple cases!
However every callback adds a level of nesting, and when you have lots of callbacks, the code starts to be complicated very quickly:
window.addEventListener('load', () => {
document.getElementById('button').addEventListener('click', () => {
setTimeout(() => {
items.forEach(item => {
//your code here
})
}, 2000)
})
})
This is just a simple 4-levels code, but I've seen much more levels of nesting and it's not fun. This nesting of call-backs is termed as a Call-back Hell
problem in JS world.
How do we solve this?
Alternatives to callbacks
Starting with ES6, JavaScript introduced several features that help us with asynchronous code that do not involve using callbacks: Promises (ES6)
and Async/Await (ES2017).
Thank You for reading the article, I hope you got to learn something new from it : )
Top comments (0)