I am developing an Excel add-in using the Office JS API. I have a taskpane which is running code similar to the following:
var failA = true
var failB = true
// In reality this is Excel.run, and it injects a context
async function run(f) {
f()
}
// Simulate a failing async call
async function fail(message, delay) {
setTimeout(()=>{
throw new Error(message)
}, delay)
}
// Simulate a successful async call
async function success(message, delay) {
setTimeout(delay)
}
async function doA() {
console.log("Inside A");
if (failA) {
console.log("Failing A");
await fail("Error A", 1000)
} else {
success("Success A")
}
console.log("Done A")
}
async function doB() {
console.log("Inside B");
if (failB) {
console.log("Failing B");
await fail("Error B", 1000)
} else {
success("Success B")
}
console.log("Done B")
}
async function main () {
try {
// This is how Excel.run is called in all the Office samples
await run(async ()=>{
console.log("Start main");
await doA();
console.log("Between A and B");
await doB();
console.log("Finished");
})}
catch (error) {
console.log("ERROR: " + error.message)
}
}
// Need to await main in an async context. In reality main is run from a button
(async () => await main())()
.as-console-wrapper { min-height: 100%!important; top: 0; }
I would expect the error in doA
to bubble up and interrupt further execution of doB
. The output should then be:
Start main
Inside A
Failing A
ERROR: Error A
Instead what I get is:
Start main
Inside A
Failing A
Done A
Between A and B
Inside B
Failing B
Done B
Finished
followed by two uncaught exceptions Error A
and Error B
.
What am I doing wrong? Can I achieve the behavior I expect without wrapping doA
and doB
separately in try...catch
blocks?
>Solution :
There are a few issues:
-
fail
is not simulating a failing async call. It returns an immediately fulfilling promise. ThesetTimeout
callback that will later throw an error, is irrelevant for that promise, since that callback executes from a fresh call stack. Similarly,success
is initiating asetTimeout
that has no relevance to the promise it returns, which again gets fulfilled immediately. -
run
is not linking its resolution to the fate of the promise returned byf
and thusrun
returns a promise that fulfills whilef
is not monitored for errors. Iff
rejects,run
will not have captured it. -
If you configure your script to execute
success
then you need toawait
it, otherwise its delay will have no effect.
Here is a fix:
var failA = true;
var failB = true;
async function run(f) {
// Link to the returned promise, so error handling around
// run() will deal with rejections
return f();
}
// Helper function
const expire = ms => new Promise(resolve => setTimeout(resolve, ms));
async function fail(message, delay) {
await expire(delay);
// throw must happen in the execution context of function fail
throw new Error(message);
}
async function success(message, delay) {
// Wait for the delay to expire, otherwise it is useless
await expire(delay);
}
async function doA() {
console.log("Inside A");
if (failA) {
console.log("Failing A");
await fail("Error A", 1000);
} else {
await success("Success A"); // Must await it
}
console.log("Done A");
}
async function doB() {
console.log("Inside B");
if (failB) {
console.log("Failing B");
await fail("Error B", 1000);
} else {
await success("Success B"); // Must await it
}
console.log("Done B");
}
async function main () {
try {
await run(async ()=>{
console.log("Start main");
await doA();
console.log("Between A and B");
await doB();
console.log("Finished");
})
} catch (error) {
console.log("ERROR: " + error.message);
}
}
main();