Re-imagining Async/Await polyfills
Tags: webdev, javascript, async, promises
Published: 2023-08-22 12:24:11

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

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

  return response.json();
}
```

Transforming it should give us:

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

    return response.json();
  });
}
```

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:

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

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

    return response.json();
  });
}
```
### In need of previous results

```javascript
async function stuff(input) {
  const response = await api.put(input);
  const extractedData = await extractData(response);
  return {
    response,
    data: extractedData,
  };
}
```

This should become:

```javascript
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,
      };
    });
}
```

### Scope-dependent exception handling

```javascript
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);
  }
}
```

This should become:

```javascript
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);
      });
    });
}
```

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

### Finally

```javascript
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);
  }
}
```

This should become:

```javascript
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);
      });
    });
}
```

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

```javascript
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;
      });
    });
}
```