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();
}
Transforming it should give us:
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:
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
async function stuff(input) {
const response = await api.put(input);
const extractedData = await extractData(response);
return {
response,
data: extractedData,
};
}
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,
};
});
}
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);
}
}
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);
});
});
}
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);
}
}
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);
});
});
}
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;
});
});
}
Top comments (1)
Great Article!