Asynchronous Javascript
Asynchronous JS
JS has only one thread, so it cannot handle multiple tasks at the same time. But thanks to the non-blocking behaviour, we can carry on to the next task before the last task is finished, this is Asynchronous Javascript.
HTTP/HTTPS
When making communication with a server, we use HTTP/HTTPS requests.
What happens when we make an HTTP/HTTPS request:
Parse URL and check cache: to make a request we need to know the url of the server, so we need to parse the URL(
https://example.com:443/path?query=foo#hash) into:protocol: https/http
host: example.com
port: 443 (default for HTTPS) or 80 (default for HTTP)
path: /path
query string: ?query=foo
DNS(Domain Name System) lookup: using DNS to get the server’s ip address.
TCP and three-way handshake: SYN SYN-ACK ACK
TLS handshake(optional): for HTTPS only, to encrypt the request.
Send request
Server receive and process request
Server return a response to be parsed by browser
The key here is to send and receive the HTTP/HTTPS request via the browser.
So what is inside the request and the server’s response?
Differ domain. ip address and URL:
- IP Address: The real numeric address of the server
- Domain: The human-readable name mapped to an ip address. The mapping is stored in DNS server.
- URL(Uniform Resource Locator): The complete address including protocol, domain, path, etc.
HTTP Request
Inside the HTTP request we have:
Request Line: This is the first line of the request, including:
- Request Method: GET/POST/DELETE/PATCH/PUT/…
- Path/URL
- Version: HTTP/HTTPS version
eg:
GET /search?q=monash HTTP/1.1Request Headers: give the server context and metadata. Common headers are:
- Host: tell the server which domain you want to search, as most server hosts multiple domains.
- User-agent: Give information on your browser, your operating system, etc.
- Accept: Tells the server what format your browser is willing to accept. Like JSON/image/HTML/…
- Authorization: Provide information on who you are. Like API keys.
Request Body: Optional. Usually PUT/POST/PATCH requests have request body.
- It may contain form data, JSON files or other file uploads.
HTTP Response
A HTTP Response is usually consists of:
- Status line:
HTTP/1.1 200 OKcontaining:- HTTP/HTTPS Version
- Status Code: Success/Redirect/Server Error/Client Error
- Status Text: human-readable text, Like
200 OK
Status Codes:
2xx — Success
Code Meaning 200 OK Standard success 201 Created New resource created (POST) 204 No Content Success, but no body 3xx — Redirects
Code Meaning 301 Moved Permanently Permanent redirect 302 Found Temporary redirect 304 Not Modified Browser can use cache
4xx — Client Errors
Code Meaning 400 Bad Request Invalid request (wrong syntax) 401 Unauthorized Need login token 403 Forbidden You are not allowed 404 Not Found Page doesn’t exist 429 Too Many Requests Rate limit
5xx — Server Errors
Code Meaning 500 Internal Server Error Server crashed 502 Bad Gateway Reverse proxy problem 503 Service Unavailable Server temporarily down 504 Gateway Timeout Upstream server took too long
- Response Headers: Headers give metadata about the response, including content information, cache control, authentication, etc.
- Response Body: Optional. contains real content, including file response, JSON API response, or empty response for
204 No Content
AJAX (Asynchronous JS and XML)
AJAX is a technique that allows a webpage to request data from a server without reloading the entire page.
It lets websites update only part of the page (like search suggestions, comments, likes) while the rest stays the same.
For now, the most used AJAX technique to communicate with servers is XHR(XMLHttpRequest) and Fetch API.
Using XHR
Steps:
- Create XHR object
1
const xhr = new XMLHttpRequest();
- Set up the request
1
xhr.open(method, url)
- Listen for load event
1
2
3
4
5
6
7
8
9xhr.addEventListener("load", ()=>{});
//or
xhr.onload = function () {
if (xhr.status === 200) {
console.log("Response:", xhr.responseText);
} else {
console.log("Request failed:", xhr.status);
}
}; - Send the request If using POST request to send data
1
xhr.send()
1
xhr.send(JSON.stringify({data: "data"}))
Useful properties:
xhr.response:could be JSON or other file typesxhr.status: the response status, like 200 OK- Progress event:
1
2
3 xhr.onprogress = (e) => {
console.log(`Loaded: ${e.loaded} / ${e.total}`);
}- Error event:
1
2 xhr.onerror = () => console.error("Network error");
Callback Hell
In real development, we may need nested callback functions, that is to use the result of the inner callback function as parameter for the outer ones. Doing so makes the code very hard to maintain and read.
Promise and Fetch
A Promise is an object that represents a value that may arrive now, later, or never.
A promise can be in three states: pending fulfilled and rejected
pending: the promise hasn’t returned a value yetfulfilled: the promise returned a valuerejected: the promise returned an error
We can create a promise object manually, but usually the promises are created automatically when we are dealing with asynchronous operations, like fetch.
Chaining a promise
We can handle the promise using then, catch and finally
1 | promise |
.thenhappens when the promise is fulfilled, by default it returns a new promise. That is what enables promise chaining:
1 | promise |
.catchhappens when any promise in the chain is rejected..finallyhappens after the promise is fulfilled or rejected
Fetch
fetch() is a modern, promise-based API used to make HTTP requests (GET, POST, PUT, DELETE, etc.).
By default, a fetch() returns a promise.
The syntax of a fetch request is :
1 | fetch(url, { |
Doing so returns a promise, and if successful, the promise will return an http response.
1 | fetch(url) |
Note that event if the request is not successful, as long as the server returns a response, the fetch promise is fulfilled.
Error Handling
For now, if the fetch promise is rejected, the error can be handled by .catch. This usually happens when the request is CORS blocked, or have no internet connections.
But receiving a failed response, like 404, is also not desirable. So we can throw an error manually when that happens:
1 | fetch("/api/data") |
The error object: there are many ways to create an error object manually:
- Basic error: throw new Error(“message”)
- Specific type: throw new TypeError(“msg”), there are also SyntaxError, ReferenceError, etc
- Custom class: class MyError extends Error {…}
Async Await
In the previous section, we used promise chaining to handle async operations, but now we have a more elegant way of writing it.
Syntax:
1 | async function hello() { |
When we add async to a function, that function will automatically return a promise.
We can use await to get the result of the promise.
It can be used in fetch operations:
1 | async function getIP() { |
We can assume that await is just like .then()
try…catch
In promise chaining we use .catch for error handling, in async-await, we also have similar feature.
1 | async function loadData() { |
try catch is just like .catch() in promise chaining.
Tips:
- only use await in an
asyncfunction- await most be followed with a promise
- async functions always return a promise
- await doesn’t block the main thread, it only stops the current function
Running multiple promises
If we simply use promise chaining, or async await, the promises are handled one by one. But sometimes, when the promises do not rely on each other, and the order doesn’t matter, we want to get the result of multiple promises at once.
- Using
Promise.all:
1 | Promise.all([ |
Using it, we can get the result of the three results at once. It returns an array of all promise results.
- Using
Promise.allSettled()
However, if any of the promises is rejected, the Promise.all will be rejected as a whole.
In order to get the results even if some promises are rejected, we can use Promise.allSettled():
1 | Promise.allSettled([p1, p2, p3]) |
- Using
Promise.race()
Given an array of promises, Promise.race() returns the that promise that gives an result(no matter rejected or fulfilled).
1 | Promise.race([slow, fast]).then(console.log); // "fast" |
- Using
Promise.any()
Given an array of promises, Promise.any() returns the first fulfilled promise. If all the promises are rejected, it will return a AggregateError.
1 | const p1 = Promise.reject("fail 1"); |
Top level await
After ES2022, we can use await outside the async function. That is top level await. But there are restraints:
Top level await can only be used in modules, this includes:
.mjsfilejsfiles with<script type="module">or"type": "module"
It’s usage is quite the same, following the await should be a promise.
Note that the top level await will block the module from loading.



