Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Error management in nested async function calls

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:

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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:

  1. fail is not simulating a failing async call. It returns an immediately fulfilling promise. The setTimeout 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 a setTimeout that has no relevance to the promise it returns, which again gets fulfilled immediately.

  2. run is not linking its resolution to the fate of the promise returned by f and thus run returns a promise that fulfills while f is not monitored for errors. If f rejects, run will not have captured it.

  3. If you configure your script to execute success then you need to await 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();
Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading