Home > Blockchain >  JavaScript: differences between async error handling with async/await and then/catch
JavaScript: differences between async error handling with async/await and then/catch

Time:12-20

Just wanted to preemptively say that I am familiar with async/await and promises in JavaScript so no need to link me to some MDN pages for that.

I have a function to fetch user details and display it on the UI.


async function someHttpCall() {
  throw 'someHttpCall error'
}
async function fetchUserDetails() {
  throw 'fetchUserDetails error'
}

function displayUserDetails(userDetails) {
  console.log('userDetails:', userDetails)
}

async function fetchUser() {
  try {
    const user = await someHttpCall()
    try {
      const details = await fetchUserDetails(user)
      returndisplayUserDetails(details)
    } catch (fetchUserDetailsError) {
      console.log('fetching user error', fetchUserDetailsError)
    }
  } catch (someHttpCallError) {
    console.log('networking error:', someHttpCallError)
  }
}

It first makes HTTP call via someHttpCall and if it succeeds then it proceeds to fetchUserDetails and it that succeeds as well then we display the details on Ui via returndisplayUserDetails.

If someHttpCall failed, we will stop and not make fetchUserDetails call. In other words, we want to separate the error handling for someHttpCall and it’s data handling from fetchUserDetails

The function I wrote is with nested try catch blocks which doesn't scale well if the nesting becomes deep and I was trying to rewrite it for better readability using plain then and catch

This was my first atttempt

function fetchUser2() {
  someHttpCall()
    .then(
      (user) => fetchUserDetails(user),
      (someHttpCallError) => {
        console.log('networking error:', someHttpCallError)
      }
    )
    .then(
      (details) => {
        displayUserDetails(details)
      }, //
      (fetchUserDetailsError) => {
        console.log('fetching user error', fetchUserDetailsError)
      }
    )
}

The problem with this is that the second then will run i.e. displayUserDetails even with someHttpCall failing. To avoid this I had to make the previous .catch blocks throw

so this is the updated version

function fetchUser2() {
  someHttpCall()
    .then(
      (user) => fetchUserDetails(user),
      (someHttpCallError) => {
        console.log('networking error:', someHttpCallError)
        throw someHttpCallError
      }
    )
    .then(
      (details) => {
        displayUserDetails(details)
      }, //
      (fetchUserDetailsError) => {
        console.log('fetching user error', fetchUserDetailsError)
      }
    )
}

However now the second catch will get called as a result of the throw. So when the someHttpCall failed, after we handled the someHttpCallError error, we would enter this block (fetchUserDetailsError) => { console.log('fetching user error', fetchUserDetailsError) } which is not good since fetchUserDetails never gets called so we shouldn't need to handle fetchUserDetailsError (I know someHttpCallError became fetchUserDetailsError in this case)

I can add some conditional checks in there to distinguish the two errors but it seems less ideal. So I am wondering how I can improve this by using .then and .catch to achieve the same goal here.

CodePudding user response:

I can add some conditional checks in there to distinguish the two errors but it seems less ideal.

Actually, that sounds like an ideal situation. That means that you don't have to nest any try / catch blocks which could make you code a lot more readable. This is one of the things that async / await is meant to solve.

A solution could be is to create custom errors by extending the Error interface to be able to determine how and where the error occurs.

class CustomError extends Error {
  constructor(name, ...args) {
    super(...args)
    this.name = name
  }
}

Throw your errors within the functions that correspond with the error.

async function someHttpCall() {
  throw new CustomError('HttpCallError', 'someHttpCall error');
}

async function fetchUserDetails(user) {
  throw new CustomError('UserDetailsError', 'fetchUserDetails error')
}

Now you can control your error flow by checking the name property on the error to differentiate your errors.

async function fetchUser() {
  try {
    const user = await someHttpCall()
    const details = await fetchUserDetails(user)
    return displayUserDetails(details)
  } catch (error) {
    switch(error.name) {
      case 'HttpCallError':
        console.log('Networking error:', error)
        break

      case 'UserDetailsError':
        console.log('Fetching user error', error)
        break
    }
  }
}

CodePudding user response:

I am wondering how I can improve this by using .then and .catch to achieve the same goal here

You don't get to avoid the nesting if you want to replicate the same behaviour:

function fetchUser2() {
  return someHttpCall().then(
    (user) => {
      return fetchUserDetails(user).then(
        (details) => {
          return displayUserDetails(details)
        },
        (fetchUserDetailsError) => {
          console.log('fetching user error', fetchUserDetailsError)
        }
      )
    },
    (someHttpCallError) => {
      console.log('networking error:', someHttpCallError)
      throw someHttpCallError
    }
  )
}

(The exact equivalent to try/catch would use .then(…).catch(…) instead of .then(…, …), but you might not actually want that.)

The function I wrote is [nested] which doesn't scale well if the nesting becomes deep and I was trying to rewrite it for better readability […]

For that, I would recommend to combine await with .catch():

async function fetchUser() {
  try {
    const user = await someHttpCall().catch(someHttpCallError => {
      throw new Error('networking error', {cause: someHttpCallError});
    });
    const details = await fetchUserDetails(user).catch(fetchUserDetailsError => {
      throw new Error('fetching user error', {cause: fetchUserDetailsError});
    });
    return displayUserDetails(details);
  } catch (someError) {
    console.log(someError.message, someError.cause);
  }
}

(The cause option for Error is still quite new, you might need a polyfill for that)

CodePudding user response:

I've been inspired by Rust's Result type (which forces you to handle every potential error along the way).

So what I do is handle exceptions in every individual function, and never allow one to throw, instead returning either an Error (if something went wrong) or the desired return value (if no exception occurred). Here's an example of how I do it (comments included):

TS Playground

If you aren't familiar with TypeScript, you can see the JavaScript-only version of the following code (with no type information) at the TypeScript Playground link above (on the right side of the page).

// This is the code in my exception-handling utility module:
// exception-utils.ts

export type Result <T = void, E extends Error = Error> = T | E;

export function getError (value: unknown): Error {
  return value instanceof Error ? value : new Error(String(value));
}

export function isError <T>(value: T): value is T & Error {
  return value instanceof Error;
}

export function assertNotError <T>(value: T): asserts value is Exclude<T, Error> {
  if (value instanceof Error) throw value;
}
// This is how to use it:
// main.ts

import {assertNotError, getError, isError, type Result} from './exception-utils.ts';

/** 
 * Returns either Error or string ID,
 * but won't throw because it catches exceptions internally
 */
declare function getStringFromAPI1 (): Promise<Result<string>>;

/** 
 * Requires ID from API1. Returns either Error or final number value,
 * but won't throw because it catches exceptions internally
 */
declare function getNumberFromAPI2 (id: string): Promise<Result<number>>;

/**
 * Create version of second function with no parameter required:
 * Returns either Error or final number value,
 * but won't throw because it catches exceptions internally
 * 
 * The previous two functions work just like this, using the utilities
 */
async function fetchValueFromAPI2 (): Promise<Result<number>> {
  try {
    const id = await getStringFromAPI1(); // Error or string
    assertNotError(id); // throws if `id` is an Error
    return getNumberFromAPI2(id); // Error or number
  }
  catch (ex) {
    return getError(ex);
  }
}

async function doSomethingWithValueFromAPI2 (): Promise<void> {
  const value = await fetchValueFromAPI2(); // value is number or Error
  if (isError(value)) {
    // handle error
  }
  else console.log(value); // value is number at this point
}
  • Related