Promises
Handling Promise objects
fetch calls are asynchronous and they return Promise objects. A promise is an object representing the completion of an asynchronous operation in future: the operation either succeeds (fulfilled) of fails (rejected).
The end result of a promise is not immediately available, and we do not know if the operation will succeed or not, nor do we know when it will be available. For that reason, promise objects need special handling:
- The handling needs to be launched when the value of the promise is available
 - We need to handle both successful results as well as failures
 
JavaScript & TypeScript provides two ways to handle promises
then-catch
Promises can be handled using the Promise then and catch methods:
fetch('https://mydomain.com/api')
    .then(response => response.json())  // handle Promise success, handling returns Promise
    .then(data => {                     // handle the latter Promise
        // process response
    })
    .catch(error => console.error(error));   // handle failures
When the promise resolves successfully, the result is passed as an argument to the handler function in the then call. then calls can be chained; the previous handler returns another promise. If the promise fails or there is any other error during the handling, the handler function passed in the catch is called.
async-await
ECMAScript 2017 added a new syntax for handling Promise objects. The purpose of the addition was to make asynchronous code easier to write and to read afterwards by making async code look more like old-school synchronous code.
You have to put async keyword in front of a function declaration thet returns a Promise instead of returning the value directly
await can be put in front of any async function call to pause your code on that line until the promise fulfills, then return the resulting value
Errors are handled as exceptions, that is using try-catch blocks
async-await example
fetchData = async () => {
  try {
    // execution is paused until the fetch call result is available
    const response = await fetch('https://mydomain.com/api');
    // execution is paused until the json call result is available
    const data = await response.json();
  }
  catch(error) {    // any error results in exception, handled here
    console.error(error);
  }
}
asynchronous code is within a try block. If any failure occurs, it will be handled in the catch handler.  Note: await only works inside async functions! Even though the code looks like synchronous it is still asynchronous.
Using async-await in React code
Both syntaxes are equally valid and usable, you can use either. Note that the function passed to useEffect may not be async.  If you need to call an async function with useEffect, pass it a sychronous function that calls your asynchronous function, e.g.
useEffect(() => {   // regular non-async function passed to useEffect
    // define async function that makes the asyncronous calls
    const doFetch = async () => {   
        const result = await fetchData();  // call my async function 
        setData(result);
    };
    // call the async function
    doFetch();
  }, []);
Immediate invokation
You can do the same as previously also using immediate invokation (calling) of the newly declared function:
useEffect(() => {   // regular non-async function passed to useEffect
    // define async function expression that makes the asynchronous calls...
    (async () => {   
        const result = await fetchData();  // call my async function 
        setData(result);
    })(); // ...and call it immediately
  }, []);
Because the precedence of the function call operator () is so high, the function expression (arrow function) needs to be in brackets