ASYNC & AJAX

What is ASYNC

  • JavaScript has a 'runtime'
  • A runtime is a process (running function) that starts in C
  • It has some resources injected into it from C
  • The runtime is 'single-threaded', which means that C only gives it the ability to do one thing at a time
    • Other programming languages can run multiple things at a time, on multiple threads - this is called 'concurrency'
  • When computing, JS orders its work in a stack which you've seen, where each new function gets an environment to run in and if one function calls another then the caller is paused whilst the callee runs.
  • Naturally, JS code runs one line after another (aka 'synchronously')
  • What happens if you need to do work that takes a long time [and may have an uncertain outcome]?

What is ASYNC cont...

  • Many things can take a long time (or have an uncertain future), like image processing, network requests, etc.
  • Whilst they are running the whole process is stuck.
  • This means that the UI of the web page doesn't work. Buttons don't click, logs don't get written, etc.
  • If this is all we had it would be disastrous BUT
  • If you look at the V8 codebase and search for 'setTimeout', 'XMLHTTPRequest', etc. you'll find they are not there
  • You will find them listed on MDN under 'Web APIs'
  • See, the JS environment of the browser is more than just the JS engine/runtime
  • Knowing that these things take a long time the browser provides these extra resources which can get threads of their own!
  • You can use these to temporarily offload work to keep your page responsive

What is ASYNC cont..

  • When you move work from the stack to certain web APIs you remove it from the stack
  • The web APIs can only give their results back when the main stack is clear (otherwise it's totally unpredictable)
  • So, there is an idea of time/chronology in the code, in that one line will run after another
    • taking code out of that timeline and putting it back at the end breaks that synchronicity, making something async[hronous]
    • demo

Stack, Web APIs, Queue and Event Loop

The Call Stack

Order of events

  • Browser runs code by pushing tasks onto the stack
  • Browser finds something that uses a web api which is async
  • Browser pushes workload off the call stack to the Web API and continues working on the rest of the stack
  • When Web API is done it returns code to the message queue
  • All the time, a small piece of code called the event loop monitors the stack to see if it is free of synchronous tasks...
  • When it is free, it feeds the next piece of work (which is generally a function you gave to a Web API) back onto the stack
    • (N.B. the Web API will have injected its result into the function)

SideNote: Tasks Queues

  • You have synchronous code - this will execute first
  • Then there are 3 types of task queue
    • [Macro] Tasks (BIG programming tasks, e.g. web API calls)
      • Execute one at start of each iteration (tick) of event loop
      • is render-blocking 
    • Microtasks (e.g. promise resolutions or DOM Mutations)
      • Completes ALL microtasks, inc. those created during
      • You can create a microtask yourself using queueMicrotask (like a 0 duration setTimeout)
      • is render-blocking
    • Animation Callbacks (e.g. requestAnimationFrame aka RAF)
      • RAF calls created before this tick commences are completed before the next tick is run, new ones are queued for next
      • NOT render-blocking

The Full Event Loop

Event

Load script

setTimeout

Mutation Obs.

queueMicrotask

render

render

render

Resolve Promise

MacroTask

MicroTask

Tick 1

Tick 2

Tick 3

So with async code

  • Code after it will run and complete before it resolves
  • Functions and their environments will finish and close without waiting
  • In summary: We cannot guarantee order of return
  • This is CHAOS!

How to handle chaos

Ways to make async, sync

  • Not having synchronicity is a problem
    • What if you need the result of that work to do further work?
  • We have come across 3 ways of making this easy on ourselves:
    1. Callbacks
    2. Promises
    3. async/await
  • Demo (github link. Please download and extract the zip)

Callbacks

Callbacks

// If these functions have asnyc components they will not work
const result1 = apiCall1();

// result1 won't be back yet, so it will be undefined
console.log(result1)


// What if instead of passing a result we passed a function to apiCall1
// to run when it had a result
function callback(result1){
	console.log(result1)
}

// Now when the call is finished apiCall1 can pass the result to the callback
apiCall1(callback);
  • Well, this is great!
  • We can get some order
  • In fact, we could do that repeatedly and all our probs are solved!!!!

Hmmmm. Maybe not...

Callback Hell -->

Promises

Promises

  • Is there any way to do that but in a flatter, more readable way and one with error handling?
  • Yes, promises...
api1Call() // executing this returns a promise, that's why we can .then()
  .then(result1 => {
    console.log('result1', result1);
    // api2Call also returns a promise, that's why we can .then() again
    return api2Call(result1); // <-- the result of this call
  })
  .then(result2 /* ends up here */ => {
    console.log('result2', result2);
  })
  .catch(function(err){
   	console.log(err)
  })
  .finally(function(){
   // ...
  });

Promise States

  • See 'States and Fates'

  • A Promise is in one of these states:

    • pending: initial state, neither fulfilled nor rejected.

    • settled

      • fulfilled: meaning that this operation was completed successfully, returning either:
        • another promise/then-able (anything with .then())
        • or a final concrete value (making the promise resolved)
          • If we cannot get a resolution then the promise is unresolved
      • rejected: meaning that the operation failed.
        • goes to the next .catch()

Promise Chains

  •  .then() or .catch()  put callback functions into an array

    • The first callback given to a then will be passed the result of the promise fullfillment

    • The next then will receive whatever that first callback returned

    • Any errors and we go to catch

  • IF we return a promise, this is called a 'promise chain'

  • We do this when we want to use the result of running one async operation when we call another (as you saw earlier)

  • If you are returning promises DO NOT add thens and catches to them without understanding that that creates a different chain and can skip parts of the original chain. (Try to avoid it where possible)

  • See demo

Making Promises

  • api1Call returned a promise (that's how then)
  • Much of the software you use will (e.g. fetch)
// We can create a promise using the Promise constructor
// We pass it a function called the 'executor'
// The executor gets 2 callbacks, one to resolve and one to reject the result
const promise = new Promise(function(resolve, reject){
	const result = somethingThatTakesALongTime();
    if (/* happy with result */) {
    	resolve(result); //<-- goes to next thens
    } else {
    	reject('Was bs mate...'); // <-- goes to catch as an error with message
    }
});


// Later in the code
promise
	.then(result => console.log(result))
    .catch(console.log(err.message));
  • You'll rarely do this
  • Web APIs and 3rd Party Software will often return you a promise
  • demo

Promises Static Methods

  • Promise.all() -  when you need all to succeed
    • Do many async things at once. Vomit if any go wrong
    • take an array of promises and return their resolved values as an array. Go to catch() if any are rejected
  • Promise.allSettled() -  seeing the results of several calls
    • Do many async things at once. DON'T Vomit if any go wrong.
    • combine to single promise result which includes rejections
  • Promise.any() - trying to get at least one success
    • Do many async things at once. Only vomit if all go wrong
    • combine to single promise. Give back first success.
  • Promise.race() - trying to get at least one response
    • Take an array of promises go with first settled promise
  • Promise.reject() - create a rejected promise
  • Promise.resolve() - create a resolved promise

Async/await

Un-then-ing

  • async adds the functionality to normal functions meaning that they return a promise
  • await waits for a promise to resolve
    • in this case, the promise from each async api call
async function(){
 try {
   const result1 = await asyncCall1();

   // call 2 depends on result of call 1
   const result2 = await asyncCall2(result1);
    
   console.log('final result', result2);
    
 } catch(err) {
   // handle error
   console.log('err', err);
 }
}

An enhancement on promises

AJAX

Asynchronous JavaScript And XML (now JSON)

AJAX Clients

  • XMLHTTPRequest
  • jQuery $.ajax()
    • just so you that it's there. You can happily use fetch, there are only a few minor differences, mostly to do with it not sending cookies/cross-site cookies
  • fetch api (isomorphic in node v.18, available in v.17 with flag)
  • axios (isomorphic - works during server-side rendering too)

Using fetch

/* fetch('http://example.com/movies.json') */

  fetch('http://example.com/movies.json', {
    method: 'POST',
    headers: {
      "Content-Type": "application/json; charset=utf-8"
      // 'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: JSON.stringify(requestBody)
  })
  .then((response) => {
     // Handle response codes here...
     if(!response.ok) throw response;
     
     return response.json();
  })
  .then((result) => {
      console.log(result);
  })
  .catch((err) => {
      console.log(err)
  });
  • On line 13 we use response.ok to check if something is a ~200 status code. You can check/handle the result like so:
    • response.status === 400, for example

Using fetch with async/await

async function callAPI(requestBody) {
  try {
    const response = await fetch("http://example.com/movies.json", {
      method: "POST",
      headers: {
        "Content-Type": "application/json; charset=utf-8"
        // 'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: JSON.stringify(requestBody)
    });

    if (!response.ok) {
      throw response;
    }
    const data = await response.json();
    renderFn(data);
  } catch (err) {
    console.log(err);
  }
}
  • You need to set Content-Type headers when sending data
  • Once you get your data you'll want to call another fn with it (e.g. line 16). It can't go back to being synchronous...

Demo API Requests

GET Request (Read)

async function callAPI() {
  try {
    const response = await fetch(
      "https://carsapp2050.fly.dev/api/v1/cars/"
    );
    
    if (!response.ok) {
      throw response;
    }
    
    const data = await response.json();
    
    // Put the data into the UI
    renderFn(data);
    
  } catch (err) {
    console.log(err);
    // show the user that an error occurred
    showError(err); 
  }
}
  • Once you get your data you'll want to call another fn with it (e.g. line 12). It can't go back to being synchronous...

POST Request (Create)

async function callAPI(newCarData /* or FormDataObject*/) {
  try {
    const response = await fetch(
      "https://carsapp2050.fly.dev/api/v1/cars/", {
      method: "POST",
      headers: {
        "Content-Type": "application/json; charset=utf-8" // <-- NECESSARY if sending data
        // 'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: JSON.stringify(newCarData),
      // body: FormDataObject,
    });
    
    if (!response.ok)  throw response;
    
    const data = await response.json();
    
    // Put the data into the UI
    renderFn(data);
    
  } catch (err) {
    console.log(err);
    // show the user that an error occurred
    showError(err); 
  }
}

PUT Request (Update)

async function callAPI(id, changes) {
  try {
    const response = await fetch(
      `https://carsapp2050.fly.dev/api/v1/cars/${id}`, {
      method: "PUT",
      headers: {
        "Content-Type": "application/json; charset=utf-8"  // <-- NECESSARY if sending data
      },
      body: JSON.stringify(changes),
    });
    
    if (!response.ok) throw response;
    
    const data = await response.json();
    
    // Put the data into the UI
    renderFn(data);
    
  } catch (err) {
    console.log(err);
    // show the user that an error occurred
    showError(err); 
  }
}

DELETE Request

async function callAPI(id) {
  try {
    const response = await fetch(
      `https://carsapp2050.fly.dev/api/v1/cars/${id}`, {
      method: "DELETE",
    });
    
    if (!response.ok) throw response;
    
    // update the UI
    renderFn();
    
  } catch (err) {
    console.log(err);
    // show the user that an error occurred
    showError(err); 
  }
}

Updating the UI

  • There are 2 options we have when we update the UI after a response:
    • optimistic updating - where we immediately adjust the UI and undo the change if the server indicates a failure to update
    • pessimistic updating  - where we wait for the server to confirm the change, then update the UI
  • ​In general I prefer pessimistic because it doesn't make false promises to a user

Showing Call State

  • Progress
    • It is traditional to show a user that a call is in progress
    • We tend to use spinners (framework or pure CSS) to show loading
      • If you're loading a fixed amount of stuff (e.g. a download) use a 'determinate' spinner, otherwise an indeterminate spinner works best
  • Errors
    • You should have a piece of UI that shows up if there is an error (e.g. a toast or alert)
    • You must inform the user that the call failed (so they're not waiting and wondering) BUT you don't have to tell them the exact technical error, you can just say "Server Call Failed - Please try again later"