DEV Community

Voltra
Voltra

Posted on

Re-imagining Async/Await polyfills

One thing I've always been curious about is: why use generators to polyfill async/await?

Sure I can see how that can be convenient, taking into account how they work in JavaScript, but why use that approach when there's much better alternatives?

The solution: Just write promises

It's as simple as it can get. Async/await are syntactic sugar for promise chains, so why not just translate them to literally that: promise chains.

Keep track of the results in a state object, that way you have access to them even in future continuation function.

The only drawback is that you do not get variable shadowing, you get variable overriding. But that could be solved trivially by using a stack-like data structure with push'n'pop mechanisms.

Examples

Let me demonstrate my proposed alternative to things like regenerator-runtime:

Simple linear transformation

async function fetchJson(url) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error("KO");
  }

  return response.json();
}
Enter fullscreen mode Exit fullscreen mode

Transforming it should give us:

function fetchJson(url) {
  return fetch(url).then(response => {
    if (!response.ok) {
       throw new Error("KO");
    }

    return response.json();
  });
}
Enter fullscreen mode Exit fullscreen mode

It's pretty much a line-by-line conversion to a simple promise-based solution.

If we want to make the transformation uniform, then it could give us:

function fetchJson(url) {
  const state = {};
  return fetch(url).then(response => {
    state.response = response;

    if (!response.ok) {
       throw new Error("KO");
    }

    return response.json();
  });
}
Enter fullscreen mode Exit fullscreen mode

In need of previous results

async function stuff(input) {
  const response = await api.put(input);
  const extractedData = await extractData(response);
  return {
    response,
    data: extractedData,
  };
}
Enter fullscreen mode Exit fullscreen mode

This should become:

function stuff(input) {
  const state = {};
  return api.put(input)
    .then(response => {
      state.response = response;
      return extractData(response);
    }).then(extractedData => {
      state.extractedData = extractedData;
      return {
        response: state.response,
        data: extractedData,
      };
    });
}
Enter fullscreen mode Exit fullscreen mode

Scope-dependent exception handling

async function stuff(input) {
  const response = await api.put(input);
  try {
    const extractedData = await extractData(response);
    return {
      response,
      data: extractedData,
    };
  } catch(e) {
    throw new ApiError(e, response);
  }
}
Enter fullscreen mode Exit fullscreen mode

This should become:

function stuff(input) {
  const state = {};
  return api.put(input)
    .then(response => {
      state.response = response;
      return extractData(response).then(extractedData => {
        state.extractedData = extractedData;
        return {
          response,
          data: extractedData,
        };
      }).catch(e => {
        throw new ApiError(e, state.response);
      });
    });
}
Enter fullscreen mode Exit fullscreen mode

You'll note that only in the case of error handling do we actually need to nest promises.

Finally

async function stuff(input) {
  const response = await api.put(input);
  try {
    const extractedData = await extractData(response);
    return {
      response,
      data: extractedData,
    };
  } catch(e) {
    throw new ApiError(e, response);
  } finally {
    logStuff(response);
  }
}
Enter fullscreen mode Exit fullscreen mode

This should become:

function stuff(input) {
  const state = {};
  return api.put(input)
    .then(response => {
      state.response = response;
      return extractData(response).then(extractedData => {
        state.extractedData = extractedData;
        return {
          response,
          data: extractedData,
        };
      }).catch(e => {
        throw new ApiError(e, state.response);
      }).finally(() => {
        logStuff(state.response);
      });
    });
}
Enter fullscreen mode Exit fullscreen mode

Or, if we want to avoid using Promise#finally and instead rely on the initial proposed API:

function stuff(input) {
  const state = {};
  return api.put(input)
    .then(response => {
      state.response = response;
      const finallyFn = () => {
        logStuff(state.response);
      };
      return extractData(response).then(extractedData => {
        state.extractedData = extractedData;
        return {
          response,
          data: extractedData,
        };
      }).catch(e => {
        finallyFn();
        throw new ApiError(e, state.response);
      }).then(_$res => {
        finallyFn();
        return _$res;
      });
    });
}
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
respect17 profile image
Kudzai Murimi

Great Article!