1 03 Javascript Async Programming
1 03 Javascript Async Programming
Programming in JS
“The” language of the Web
Fulvio Corno
Luigi De Russis
2
Applicazioni Web I - Web Applications I - 2023/2024
JavaScript: The Definitive Guide, 7th Edition
11.1 Asynchronous Programming with Callbacks
CALLBACKS
3
Applicazioni Web I - Web Applications I - 2023/2024
https://developer.mozilla.org/en-
Callbacks US/docs/Glossary/Callback_function
4
Applicazioni Web I - Web Applications I - 2023/2024
Synchronous Callbacks
• Used in functional programming
– e.g., providing the sort criteria for array sorting
console.log(numbers);
5
Applicazioni Web I - Web Applications I - 2023/2024
Synchronous Callbacks
• Example: filter according to a criteria
– filter() creates a new array with all elements for which the callback returns true
const market = [
{ name: 'GOOG', var: -3.2 },
{ name: 'AMZN', var: 2.2 },
{ name: 'MSFT', var: -1.8 }
];
FUNCTIONAL PROGRAMMING
7
Applicazioni Web I - Web Applications I - 2023/2024
Functional Programming: A Brief Overview
• A programming paradigm where the developer mostly construct and
structure code using functions
– not JavaScript's main paradigm, but JavaScript is well suited
• More “declarative style” rather than “imperative style” (e.g., for loops)
• Can improve program readability:
new_array = new_array = [] ;
array.filter ( filter_function ) ; for (const el of list)
if ( filter_function(el) )
new_array.push(el) ;
8
Applicazioni Web I - Web Applications I - 2023/2024
Notable Features of the Functional Paradigm
• Functions are first-class citizens
– functions can be used as if they were variables or constants, combined with other
functions and generate new functions in the process, chained with other functions, etc.
• Higher-order functions
– a function that operates on functions, taking one or more functions as arguments and
typically returning a new function
• Function composition
– composing/creating functions to simplify and compress your functions by taking
functions as an argument and return an output
• Call chaining
– returning a result of the same type of the argument, so that multiple functional
operators may be applied consecutively
9
Applicazioni Web I - Web Applications I - 2023/2024
Functional Programming in JavaScript
• JavaScript supports the features of the paradigm “out of the box”
• Functional programming requires avoiding mutability
– i.e., do not change objects in place!
– e.g., if you need to perform a change in an array, return a new array
10
Applicazioni Web I - Web Applications I - 2023/2024
Iterating over Arrays
• Iterators: for ... of, for (..;..;..)
• Iterators: forEach(f)
– Process each element with callback f
• Iterators: every(f), some(f)
Functional style
– Check whether all/some elements in the array satisfy the Boolean callback f
• Iterators that return a new array: map(f), filter(f)
– Construct a new array
• reduce: callback function on all items to progressively compute a result
reduce(callback( accumulator, currentValue[, index[, array]] )[, initialValue])
11
Applicazioni Web I - Web Applications I - 2023/2024
https://developer.mozilla.org/en-
US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
.forEach()
• forEach() invokes your (synchronous) callback function once for each
element of an iterable
12
Applicazioni Web I - Web Applications I - 2023/2024
https://developer.mozilla.org/en-
US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
.forEach()
• forEach() invokes your (synchronous) callback function once for each
element of an iterable
– The callback may have 3 parameters
• currentValue: The current element being processed in the array.
• index (Optional): The index of currentValue in the array
• array (Optional): The array forEach() was called upon.
– Always returns undefined and is not chainable
– No way to stop or break a forEach() loop other than by throwing an exception
• forEach() does not mutate the array on which it is called
– however, its callback may do so
13
Applicazioni Web I - Web Applications I - 2023/2024
.every()
• every() tests whether all elements in the array pass the test
implemented by the provided function
– Callback: Same 3 arguments as forEach
– It returns a Boolean value (truthy/falsy)
– It executes its callback once for each element present in the array until it finds the
one where the callback returns a falsy value
• If such an element is found, immediately returns false
let a = [1, 2, 3, 4, 5];
a.every(x => x < 10); // => true: all values are < 10
a.every(x => x % 2 === 0); // false: not all even values
14
Applicazioni Web I - Web Applications I - 2023/2024
.some()
• some() tests whether at least one element in the array passes the test
implemented by the provided function
– It returns a Boolean value
– It executes its callback once for each element present in the array until it finds the
one where the callback returns a truthy value
• if such an element is found, immediately returns true
15
Applicazioni Web I - Web Applications I - 2023/2024
.map()
• map() passes each element of the array on which it is invoked to the
function you specify
– the callback should return a value
– map() always returns a new array containing the values returned by the callback
console.log(b); // [1, 4, 9]
console.log(uppercase.join(''));
16
Applicazioni Web I - Web Applications I - 2023/2024
.filter()
• filter() creates a new array with all elements that pass the test
implemented by the provided function
– the callback is a function that returns either true or false
– if no element passes the test, an empty array is returned
a.filter(x => x < 3); // generates [2, 1], values less than 3
17
Applicazioni Web I - Web Applications I - 2023/2024
reduce(
.reduce()
callback(accumulator, currentValue[, index[, array]])
[, initialValue]
)
18
Applicazioni Web I - Web Applications I - 2023/2024
.reduce()
const a = [5, 4, 3, 2, 1];
• Callbacks used with reduce() are
different than the ones used with a.reduce( (accumulator, currentValue) =>
forEach() and map() accumulator + currentValue, 0);
// 15; the sum of the values
– the first argument is the accumulated
result of the reduction so far
a.reduce((acc, val) => acc*val, 1);
– on the first call to this function, its first
// 120; the product of the values
argument is the initial value
– on subsequent calls, it is the value
a.reduce((acc, val) => (acc > val) ? acc
returned by the previous invocation of
: val);
the reducer function
// 5; the largest of the values
19
Applicazioni Web I - Web Applications I - 2023/2024
20
Applicazioni Web I - Web Applications I - 2023/2024
JavaScript: The Definitive Guide, 7th Edition
Chapter 11. Asynchronous JavaScript
ASYNCHRONOUS PROGRAMMING
21
Applicazioni Web I - Web Applications I - 2023/2024
Asynchronicity
• JavaScript is single-threaded and inherently synchronous
– i.e., code cannot create threads and run in parallel in the JS engine
• Callbacks are the most fundamental way for writing asynchronous JS
code
• How can they work asynchronously? const deleteAfterTimeout = (task) =>
– e.g., how can setTimeout() or {
other async callbacks work? // do something
}
• Thanks to the Execution Environment // runs after 2 seconds
– e.g., browsers and Node.js setTimeout(deleteAfterTimeout, 2000,
• and the Event Loop task)
22
Applicazioni Web I - Web Applications I - 2023/2024
Non-Blocking Code!
• Asynchronous techniques are very useful, particularly for web development
• For instance: when a web app runs executes an intensive chunk of code
without returning control to the browser, the browser can appear to be frozen
– this is called blocking, and it should be the exception!
• the browser is blocked from continuing to handle user input and perform other tasks until the web
app returns control of the processor
• This may happen outside browsers, as well
– e.g., reading a long file from the disk/network, accessing a database and returning data,
accessing a video stream from a webcam, etc.
• Most of the JS execution environments are, therefore, deeply asynchronous
– with non-blocking primitives
– JavaScript programs are event-driven, typically
23
Applicazioni Web I - Web Applications I - 2023/2024
Asynchronous Callbacks
• The most fundamental way for writing asynchronous JS code
• Great for “simple” things!
const readline = require('readline');
• Handling user actions
– e.g., button click const rl = readline.createInterface({
input: process.stdin,
• Handling I/O operations output: process.stdout
});
– e.g., fetch a document
rl.question('How old are you? ', (answer) => {
• Handling time intervals let description = answer;
– e.g., timers
rl.close();
• Interfacing with databases });
24
Applicazioni Web I - Web Applications I - 2023/2024
Timers
• To delay the execution of a function:
– setTimeout() runs the callback function after a given period of time
– setInterval() runs the callback function periodically
clearInterval(id) ;
26
Applicazioni Web I - Web Applications I - 2023/2024
Handling Errors in Callbacks
• No “official” ways, only best practices!
• Typically, the first parameter of the callback function is for storing any
error, while the second one is for the result of the operation
– this is the strategy adopted by Node.js, for instance
fs.readFile('/file.json', (err, data) => {
if (err !== null) {
console.log(err);
return;
}
//no errors, process data
console.log(data);
});
27
Applicazioni Web I - Web Applications I - 2023/2024
Data Persistence
28
Applicazioni Web I - Web Applications I - 2023/2024
Server-Side Persistence
• A web server should normally store data into a persistent database
• Node supports most databases
– Cassandra, Couchbase, CouchDB, LevelDB, MySQL, MongoDB, Neo4j, Oracle,
PostgreSQL, Redis, SQL Server, SQLite, Elasticsearch
• An easy solution for simple and small-volume applications is SQLite
– in-process on-file relational database
29
Applicazioni Web I - Web Applications I - 2023/2024
SQLite
• Uses the ‘sqlite’ npm module
• Documentation: https://github.com/mapbox/node-sqlite3/wiki
...
db.close();
30
Applicazioni Web I - Web Applications I - 2023/2024
SQLite: Queries rows.forEach((row) => {
console.log(row.name);
});
https://www.sqlitetutorial.net/sqlite-nodejs/
31
Applicazioni Web I - Web Applications I - 2023/2024
SQLite: Queries
rows.forEach((row) => {
console.log(row.name);
});
https://www.sqlitetutorial.net/sqlite-nodejs/
32
Applicazioni Web I - Web Applications I - 2023/2024
SQLite: Other Queries
• db.run(sql, [params], function (err) { } )
– For statement that do not return a value
– CREATE TABLE
– INSERT
– UPDATE
– In the callback function
• this.changes == number of affected rows
• this.lastID == number of inserted row ID (for INSERT queries)
• Note: To make this work correctly in the callback, the arrow function syntax cannot be used here
https://www.sqlitetutorial.net/sqlite-nodejs/
33
Applicazioni Web I - Web Applications I - 2023/2024
Parametric Queries
• The SQL string may contain parameter placeholders: ?
• The placeholders are replaced by the values in the [params] array
– in order: one param per each ?
34
Applicazioni Web I - Web Applications I - 2023/2024
Example
Table: course Table: score
35
Applicazioni Web I - Web Applications I - 2023/2024
Example
transcript.mjs
import sqlite from 'sqlite3';
const db = new sqlite.Database('transcript.sqlite',
(err) => { if (err) throw err; });
36
Applicazioni Web I - Web Applications I - 2023/2024
Example code: '01TYMOV',
{
37
Applicazioni Web I - Web Applications I - 2023/2024
But…
import sqlite from 'sqlite3';
const db = new sqlite.Database('transcript.sqlite', (err) => { if (err) throw err; });
38
Applicazioni Web I - Web Applications I - 2023/2024
Queries Are Executed Asynchronously
CREATE TABLE IF NOT EXISTS "numbers" (
"number" INTEGER
);
INSERT INTO "numbers" ("number") VALUES (1);
39
Applicazioni Web I - Web Applications I - 2023/2024
Queries Are Executed Asynchronously
import sqlite from 'sqlite3';
…
const db = new sqlite.Database('data.sqlite',
389
(err) => { if (err) throw err; });
390
for(let i=0; i<100; i++) { 391
db.run('insert into numbers(number) values(1)', 392
(err) => { if (err) throw err; }); 396
396
db.all('select count(*) as tot from numbers', 396
(err, rows) => { 397
if(err) throw err; 398
console.log(rows[0].tot); 399
}) ; 399
400
} 400
db.close(); queries.js …
40
Applicazioni Web I - Web Applications I - 2023/2024
Queries are Executed Asynchronously
import sqlite from 'sqlite3';
…
const db = new sqlite.Database('data.sqlite',
389
(err) => { if (err) throw err; });
390
for(let i=0; i<100; i++) { 391
db.run('insert into numbers(number) values(1)', 392
(err) => { if (err) throw err; }); 396
396
db.all('select count(*) as tot from numbers', 396
(err, rows) => { 397
if(err) throw err; 398
console.log(rows[0].tot); 399
}) ; 399
400
} 400
db.close(); …
41
Applicazioni Web I - Web Applications I - 2023/2024
Solution?
for(let i=0; i<100; i++) {
}) ;
42
Applicazioni Web I - Web Applications I - 2023/2024
JavaScript: The Definitive Guide, 7th Edition
Chapter 11. Asynchronous JavaScript
PROMISES
43
Applicazioni Web I - Web Applications I - 2023/2024
Beware: Callback Hell!
• If you want to perform multiple import readline from 'readline';
const rl = readline.createInterface({
asynchronous actions in a row input: process.stdin,
output: process.stdout
using callbacks, you must keep });
})
});
44
Applicazioni Web I - Web Applications I - 2023/2024
Promises
• A core language feature to “simplify” asynchronous programming
– a possible solution to callback hell, too!
– a fundamental building block for “newer” functions (async, ES2017)
• It is an object representing the eventual completion (or failure) of an
asynchronous operation
– i.e., an asynchronous function returns a promise to supply the value at some
point in the future, instead of returning immediately a final value
• Promises standardize a way to handle errors and provide a way for errors
to propagate correctly through a chain of promises
45
Applicazioni Web I - Web Applications I - 2023/2024
Promises
• Promises can be created or consumed
– many Web APIs expose Promises to be consumed!
• When consumed:
– a Promise starts in a pending state
• the caller function continues the execution, while it waits for the Promise to do its own
processing, and give the caller function some “responses”
– then, the caller function waits for it to either return the promise in a fulfilled state
or in a rejected state
46
Applicazioni Web I - Web Applications I - 2023/2024
Creating a Promise
• A Promise object is created using const myPromise =
the new keyword new Promise((resolve, reject) => {
• Its constructor takes an executor
function, as its parameter // do something asynchronous which
• This function takes two functions as eventually call either:
parameters:
– resolve, called when the resolve(someValue); // fulfilled
asynchronous task completes
successfully and returns the results of // or
the task as a value
– reject, called when the task fails and
returns the reason for failure (an error reject("failure reason"); // rejected
object, typically)
});
47
Applicazioni Web I - Web Applications I - 2023/2024
Creating a Promise
function waitPromise(duration) {
// Create and return a new promise
• You can also provide a function return new Promise((resolve, reject) => {
with “promise functionality” // If the argument is invalid,
// reject the promise
• Simply have it return a promise! if (duration < 0) {
reject(new Error('Time travel not yet
implemented'));
} else {
// otherwise, wait asynchronously and then
// resolve the Promise; setTimeout will
// invoke resolve() with no arguments:
// the Promise will fulfill with
// the undefined value
setTimeout(resolve, duration);
}
});
}
48
Applicazioni Web I - Web Applications I - 2023/2024
Consuming a Promise
• When a Promise is fulfilled, the waitPromise().then((result) => {
then() callback is used console.log("Success: ", result);
• If a Promise is rejected, instead, the }).catch((error) => {
catch() callback will handle the console.log("Error: ", error);
error });
• then() and catch() are instance
methods defined by the Promise // if a function returns a Promise...
object waitPromise(1000).then(() => {
– each function registered with then() is console.log("Success!");
invoked only once
}).catch((error) => {
• You can omit catch(), if you are console.log("Error: ", error);
interested in the result, only });
49
Applicazioni Web I - Web Applications I - 2023/2024
Consuming a Promise
• p.then(onFulfilled[, onRejected]);
– Callbacks are executed asynchronously (inserted in the event loop) when the promise
is either fulfilled (success) or rejected (optional)
• p.catch(onRejected);
– Callback is executed asynchronously (inserted in the event loop) when the promise is
rejected
– Short for p.then(null, failureCallback)
• p.finally(onFinally);
– Callback is executed in any case, when the promise is either fulfilled or rejected
– Useful to avoid code duplication in then and catch handlers
• All these methods return Promises, too! ⇒ They can be chained
50
Applicazioni Web I - Web Applications I - 2023/2024
Promise: Create & Consume
const prom = new Promise( prom
(resolve, reject) => { .then((x) => {
... ...use x...
resolve(x); } )
... .catch( (y) => {
reject(y); ...use y...
... } ) ;
}
)
51
Applicazioni Web I - Web Applications I - 2023/2024
Chaining Promises
• One of the most important benefits of getRepoInfo()
Promises .then(repo => getIssue(repo))
• They provide a natural way to express .then(issue => getOwner(issue.ownerId))
a sequence of asynchronous .then(owner => sendEmail(owner.email,
operations as a linear chain of then() 'Some text'))
invocations
.catch(e => {
– without having to nest each operation
within the callback of the previous one // just log the error
• the "callback hell" seen before console.error(e)
– If the error handling code is the same for })
all steps, you can attach catch() to the
end of the chain .finally(_ => logAction());
});
• Important: always return results,
otherwise callbacks won’t get the
result of a previous promise
52
Applicazioni Web I - Web Applications I - 2023/2024
Example: Chaining
• Useful, for instance, with I/O API such as fetch(), which returns a Promise
const status = (response) => {
if (response.status >= 200 && response.status < 300) {
return Promise.resolve(response) // static method to return a fulfilled Promise
}
return Promise.reject(new Error(response.statusText))
}
const json = (response) => response.json()
fetch('/todos.json')
.then(status)
.then(json)
.then((data) => { console.log('Request succeeded with JSON response', data) })
.catch((error) => { console.log('Request failed', error) })
53
Applicazioni Web I - Web Applications I - 2023/2024
Promise.all(promises)
Promises… in Parallel .then(results => console.log(results))
.catch(e => console.error(e));
54
Applicazioni Web I - Web Applications I - 2023/2024
JavaScript: The Definitive Guide, 7th Edition
Chapter 11. Asynchronous JavaScript
ASYNC/AWAIT
55
Applicazioni Web I - Web Applications I - 2023/2024
Simplifying Writing With async / await
• ECMAScript 2017 (ES8) introduces two new keywords, async and
await
– write promise-based asynchronous code that looks like synchronous code
• Prepend the async keyword to any function means that it will return a
Promise
• Prepend await when calling an async function (or a function returning
a Promise) makes the calling code stop until the promise is resolved or
rejected
const sampleFunction = async () => {
return 'test'
}
sampleFunction().then(console.log) // This will log 'test'
56
Applicazioni Web I - Web Applications I - 2023/2024
async Functions
• The async function declaration defines an asynchronous function
• Asynchronous functions operate in a separate order than the rest of the
code (via the event loop), returning an implicit Promise as their result
– but the syntax and structure of code using async functions looks like standard
synchronous functions.
https://developer.mozilla.org/en-
US/docs/Web/JavaScript/Reference/Statements/async_function
57
Applicazioni Web I - Web Applications I - 2023/2024
await
• The await operator can be used to wait for a Promise. It can only be used inside
an async function
• await blocks the code execution within the async function until the Promise is
resolved
• When resumed, the value of the await expression is that of the fulfilled Promise
• If the Promise is rejected, the await expression throws the rejected value
– If the value of the expression following the await operator is not a Promise, it's converted
to a resolved Promise
returnValue = await expression ;
https://developer.mozilla.org/en-
US/docs/Web/JavaScript/Reference/Operators/await
58
Applicazioni Web I - Web Applications I - 2023/2024
Example: async / await
function resolveAfter2Seconds() {
return new Promise(resolve => { Return a
setTimeout(() => { promise
resolve('resolved');
}, 2000);
});
}
async function asyncCall() { async is needed to use await
console.log('calling'); Looks like
const result = await resolveAfter2Seconds(); sequential
console.log(result); code
}
> "calling"
asyncCall(); //... 2 seconds
> "resolved"
59
Applicazioni Web I - Web Applications I - 2023/2024
Example: async / await
function resolveAfter2Seconds() {
return new Promise(resolve => {
setTimeout(() => {
resolve('resolved');
}, 2000);
});
}
async function asyncCall() { Implicitly returns a Promise
console.log('calling');
const result = await resolveAfter2Seconds();
return 'end';
}
> "calling"
Can use Promise //... 2 seconds
asyncCall().then(console.log);
methods > "end"
60
Applicazioni Web I - Web Applications I - 2023/2024
Examples… Before and After
const makeRequest = () => { const makeRequest = async () => {
return getAPIData() console.log(await getAPIData());
.then(data => { return "done";
console.log(data); };
return "done";
} let res = makeRequest();
);
}
61
Applicazioni Web I - Web Applications I - 2023/2024
Examples… Before and After
function getData() {
return getIssue()
.then(issue => getOwner(issue.ownerId))
.then(owner => sendEmail(owner.email, 'Some text'));
}
62
Applicazioni Web I - Web Applications I - 2023/2024
Chaining with async/await
• Simpler to read, easier to debug
– debugger would not stop on asynchronous code
const getFirstUserData = async () => {
const response = await fetch('/users.json'); // get users list
const users = await response.json(); // parse JSON
const user = users[0]; // pick first user
const userResponse = await fetch(`/users/${user.name}`); // get user data
const userData = await user.json(); // parse JSON
return userData;
}
getFirstUserData();
63
Applicazioni Web I - Web Applications I - 2023/2024
Promises or async/await? Both!
• If the output of function2 is dependent on the output of function1, use
await.
• If two functions can be run in parallel, create two different async functions
and then run them in parallel Promise.all(promisesArray)
• Instead of creating huge async functions with many await asyncFunction() in
it, it is better to create smaller async functions (not too much blocking code)
• If your code contains blocking code, it is better to make it an async function.
The callers can decide on the level of asynchronicity they want.
https://medium.com/better-programming/should-i-use-promises-or-async-await-126ab5c98789
64
Applicazioni Web I - Web Applications I - 2023/2024
SQLite… revisited
function insertOne() { function printCount() {
return new Promise( (resolve, reject) => { return new Promise( (resolve, reject) => {
db.run('insert into numbers(number) va db.all('select count(*) as tot from nu
lues(1)', (err) => { mbers',
if (err) reject(err); (err, rows) => {
else resolve('Done'); if(err)
}); reject(err);
}) ; else {
} console.log(rows[0].tot);
resolve(rows[0].tot);
}
}) ;
}) ;
}
65
Applicazioni Web I - Web Applications I - 2023/2024
SQLite… revisited
function insertOne() { function printCount() {
return new Promise( (resolve, reject) => { return new Promise( (resolve, reject) => {
db.run('insert into numbers(number) va db.all('select count(*) as tot from nu
lues(1)', (err) => { mbers',
if (err) reject(err); (err, rows) => {
else resolve('Done'); if(err)
}); reject(err);
}) ; else {
async function main() {
} console.log(rows[0].tot);
for(let i=0; i<100; i++) {
resolve(rows[0].tot);
await insertOne();
}
await printCount();
}) ;
}
}) ;
db.close();
}
}
main() ;
66
Applicazioni Web I - Web Applications I - 2023/2024
Beware The Bug!
async function main() {
for(let i=0; i<100; i++) {
await insertOne();
await printCount();
}
db.close();
} async function main() {
for(let i=0; i<100; i++) {
main() ; await insertOne();
await printCount();
}
}
main() ;
db.close();
67
Applicazioni Web I - Web Applications I - 2023/2024
SQLite Libraries: Various Options
• sqlite3: the basic SQLite interface (JS wrapper of the SQLite C library)
• sqlite: This module has the same API as the original sqlite3 library,
except that all its API methods return ES6 Promises.
– internally, it wraps sqlite3; written in TypeScript
• sqlite-async: ES6 Promise-based interface to the sqlite3 module.
• better-sqlite3: Easy-to-use synchronous API (they say it’s faster…)
• … search on https://www.npmjs.com/
68
Applicazioni Web I - Web Applications I - 2023/2024
License
• These slides are distributed under a Creative Commons license “Attribution-NonCommercial-
ShareAlike 4.0 International (CC BY-NC-SA 4.0)”
• You are free to:
– Share — copy and redistribute the material in any medium or format
– Adapt — remix, transform, and build upon the material
– The licensor cannot revoke these freedoms as long as you follow the license terms.
• Under the following terms:
– Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were
made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or
your use.
– NonCommercial — You may not use the material for commercial purposes.
– ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions
under the same license as the original.
– No additional restrictions — You may not apply legal terms or technological measures that legally restrict
others from doing anything the license permits.
• https://creativecommons.org/licenses/by-nc-sa/4.0/
69
Applicazioni Web I - Web Applications I - 2023/2024