Note. This text already assumes that you simply are accustomed to callbacks, guarantees, and have a basic understanding of the asynchronous paradigm in JavaScript.
The asynchronous mechanism is one of the essential concepts in JavaScript and programming usually. It allows a program to individually execute secondary tasks within the background without blocking the present thread from executing primary tasks. When a secondary task is accomplished, its result’s returned and this system continues to run normally. On this context, such secondary tasks are called asynchronous.
Asynchronous tasks typically include making requests to external environments like databases, web APIs or operating systems. If the results of an asynchronous operation doesn’t affect the logic of the fundamental program, then as an alternative of just waiting before the duty could have accomplished, it’s a lot better to not waste this time and proceed executing primary tasks.
Nevertheless, sometimes the results of an asynchronous operation is used immediately in the subsequent code lines. In such cases, the succeeding code lines mustn’t be executed until the asynchronous operation is accomplished.
Note. Before attending to the fundamental a part of this text, I would really like to supply the motivation for why asynchronicity is taken into account a very important topic in Data Science and why I used JavaScript as an alternative of Python to elucidate the
async / awaitsyntax.
Data engineering is an inseparable a part of Data Science, which mainly consists of designing robust and efficient data pipelines. Certainly one of the standard tasks in data engineering includes making regular calls to APIs, databases, or other sources to retrieve data, process it, and store it somewhere.
Imagine a knowledge source that encounters network issues and can’t return the requested data immediately. If we simply make the request in code to that service, we could have to attend quite a bit, while doing nothing. Wouldn’t or not it’s higher to avoid wasting your processor time and execute one other function, for instance? That is where the facility of asynchronicity comes into play, which will probably be the central topic of this text!
No one will deny the incontrovertible fact that Python is the preferred current selection for creating Data Science applications. Nevertheless, JavaScript is one other language with an enormous ecosystem that serves various development purposes, including constructing web applications that process data retrieved from other services. Because it seems, asynchronicity plays one of the fundamental roles in JavaScript.
Moreover, in comparison with Python, JavaScript has richer built-in support for coping with asynchronicity and frequently serves as a greater example to dive deeper into this topic.
Finally, Python has an analogous async / await construction. Subsequently, the knowledge presented in this text about JavaScript can be transferable to Python for designing efficient data pipelines.
In the primary versions of JavaScript, asynchronous code was mainly written with callbacks. Unfortunately, it led developers to a widely known problem named “callback hell”. A variety of times asynchronous code written with raw callbacks led to several nested code scopes which were extremely difficult to read. That’s the reason in 2012 the JavaScript creators introduced guarantees.
// Example of the "callback hell" problemfunctionOne(function () {
functionTwo(function () {
functionThree(function () {
functionFour(function () {
...
});
});
});
});
Guarantees provide a convenient interface for asynchronous code development. A promise takes right into a constructor an asynchronous function which is executed at a certain moment of time in the longer term. Before the function is executed, the promise is alleged to be in a pending state. Depending on whether the asynchronous function has been accomplished successfully or not, the promise changes its state to either fulfilled or rejected respectively. For the last two states, programmers can chain .then()and .catch() methods with a promise to declare the logic of how the results of the asynchronous function ought to be handled in numerous scenarios.
Aside from that, a gaggle of guarantees will be chained by utilizing combination methods like any(), all(), race(), etc.
Despite the incontrovertible fact that guarantees have grow to be a big improvement over callbacks, they’re still not ideal, for several reasons:
- Verbosity. Guarantees normally require writing quite a lot of boilerplate code. In some cases, making a promise with a straightforward functionality requires just a few extra lines of code due to its verbose syntax.
- Readability. Having several tasks depending on one another results in nesting guarantees one inside one other. This infamous problem may be very much like the “callback hell” making code difficult to read and maintain. Moreover, when coping with error handling, it will likely be hard to follow code logic when an error is propagated through several promise chains.
- Debugging. By checking the stack trace output, it is perhaps difficult to discover the source of an error inside guarantees as they don’t normally provide clear error descriptions.
- Integration with legacy libraries. Many legacy libraries in JavaScript were developed up to now to work with raw callbacks, thus not making it easily compatible with guarantees. If code is written by utilizing guarantees, then additional code components ought to be created to supply compatibility with old libraries.
For probably the most part, the async / await construction was added into JavaScript as synthetic sugar over guarantees. Because the name suggests, it introduces two recent code keywords:
asyncis used before the function signature and marks the function as asynchronous which all the time returns a promise (even when a promise will not be returned explicitly as it’ll be wrapped implicitly).awaitis used inside functions marked as async and is said within the code before asynchronous operations which return a promise. If a line of code comprises theawaitkeyword, then the next code lines contained in the async function won’t be executed until the returned promise is settled (either within the fulfilled or rejected state). This makes sure that if the execution logic of the next lines is dependent upon the results of the asynchronous operation, then they may not be run.
– The
awaitkeyword will be used several times inside an async function.– If
awaitis used inside a function that will not be marked as async, theSyntaxErrorwill probably be thrown.– The returned results of a function marked with
awaitit the resolved value of a promise.
The async / await usage example is demonstrated within the snippet below.
// Async / await example.
// The code snippet prints start and end words to the console.function getPromise() {
return recent Promise((resolve, reject) => {
setTimeout(() => {
resolve('end');
},
1000);
});
}
// since this function is marked as async, it'll return a promise
async function printInformation() {
console.log('start');
const result = await getPromise();
console.log(result) // this line won't be executed until the promise is resolved
}
It’s important to know that await doesn’t block the fundamental JavaScript thread from execution. As an alternative, it only suspends the enclosing async function (while other program code outside the async function will be run).
Error handling
The async / await construction provides a regular way for error handling with try / catch keywords. To handle errors, it’s obligatory to wrap all of the code that may potentially cause an error (including await declarations) within the try block and write corresponding handle mechanisms within the catch block.
In practice, error handling with
try / catchblocks is less complicated and more readable than achieving the identical in guarantees with.catch()rejection chaining.
// Error handling template inside an async functionasync function functionOne() {
try {
...
const result = await functionTwo()
} catch (error) {
...
}
}
async / await is a terrific alternative to guarantees. They eliminate the aforementioned shortcomings of guarantees: the code written with async / await will likely be more readable, and maintainable and is a preferable selection for many software engineers.
Nonetheless, it will be incorrect to disclaim the importance of guarantees in JavaScript: in some situations, they’re a greater option, especially when working with functions returning a promise by default.
Code interchangeability
Allow us to take a look at the identical code written with async / await and guarantees. We’ll assume that our program connects to a database and in case of a longtime connection it requests data about users to further display them within the UI.
// Example of asynchronous requests handled by async / awaitasync function functionOne() {
try {
...
const result = await functionTwo()
} catch (error) {
...
}
}
Each asynchronous requests will be easily wrapped by utilizing the await syntax. At each of those two steps, this system will stop code execution until the response is retrieved.
Since something unsuitable can occur during asynchronous requests (broken connection, data inconsistency, etc.), we must always wrap the entire code fragment right into a try / catch block. If an error is caught, we display it to the console.
Now allow us to write the identical code fragment with guarantees:
// Example of asynchronous requests handled by guaranteesfunction displayUsers() {
...
connectToDatabase()
.then((response) => {
...
return getData(data);
})
.then((users) => {
showUsers(users);
...
})
.catch((error) => {
console.log(`An error occurred: ${error.message}`);
...
});
}
This nested code looks more verbose and harder to read. As well as, we are able to notice that each await statement was transformed right into a corresponding then() method and that the catch block is now situated contained in the .catch() approach to a promise.
Following the identical logic, every
async / awaitcode will be rewritten with guarantees. This statement demonstrates the incontrovertible fact thatasync / awaitis just synthetic sugar over guarantees.
Code written with async / await will be transformed into the promise syntax where each await declaration would correspond to a separate .then() method and exception handling could be performed within the .catch() method.
On this section, we could have a glance an actual example of how async / await works.
We’re going to use the REST countries API which provides demographic information for a requested country within the JSON format by the next URL address: https://restcountries.com/v3.1/name/$country.
Firstly, allow us to declare a function that may retrieve the fundamental information from the JSON. We’re taken with retrieving information regarding the country’s name, its capital, area and population. The JSON is returned in the shape of an array where the primary object comprises all of the obligatory information. We are able to access the aforementioned properties by accessing the article’s keys with corresponding names.
const retrieveInformation = function (data) {
data = data[0]
return {
country: data["name"]["common"],
capital: data["capital"][0],
area: `${data["area"]} km`,
population: `{$data["population"]} people`
};
};
Then we are going to use the fetch API to perform HTTP requests. Fetch is an asynchronous function which returns a promise. Since we immediately need the information returned by fetch, we must wait until the fetch finishes its job before executing the next code lines. To do this, we use the await keyword before fetch.
// Fetch example with async / awaitconst getCountryDescription = async function (country) {
try {
const response = await fetch(
`https://restcountries.com/v3.1/name/${country}`
);
if (!response.okay) {
throw recent Error(`Bad HTTP status of the request (${response.status}).`);
}
const data = await response.json();
console.log(retrieveInformation(data));
} catch (error) {
console.log(
`An error occurred while processing the request.nError message: ${error.message}`
);
}
};
Similarly, we place one other await before the .json() method to parse the information which is used immediately after within the code. In case of a foul response status or inability to parse the information, an error is thrown which is then processed within the catch block.
For demonstration purposes, allow us to also rewrite the code snippet by utilizing guarantees:
// Fetch example with guaranteesconst getCountryDescription = function (country) {
fetch(`https://restcountries.com/v3.1/name/${country}`)
.then((response) => {
if (!response.okay) {
throw recent Error(`Bad HTTP status of the request (${response.status}).`);
}
return response.json();
})
.then((data) => {
console.log(retrieveInformation(data));
})
.catch((error) => {
console.log(
`An error occurred while processing the request. Error message: ${error.message}`
);
});
};
Calling an either function with a provided country name will print its fundamental information:
// The results of calling getCountryDescription("Argentina"){
country: 'Argentina',
capital: 'Buenos Aires',
area: '27804000 km',
population: '45376763 people'
}
In this text, we’ve got covered the async / await construction in JavaScript which appeared within the language in 2017. Having appeared as an improvement over guarantees, it allows writing asynchronous code in a synchronous manner eliminating nested code fragments. Its correct usage combined with guarantees leads to a strong mix making the code as clean as possible.
Lastly, the knowledge presented in this text about JavaScript can also be useful for Python as well, which has the identical async / await construction. Personally, if someone desires to dive deeper into asynchronicity, I might recommend focusing more on JavaScript than on Python. Being aware of the abundant tools that exist in JavaScript for developing asynchronous applications provides a better understanding of the identical concepts in other programming languages.
All images unless otherwise noted are by the writer.
