skip to Main Content

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?

2

Answers


  1. setTimeout does not know anything about promises. Instead use a promise based sleep for waiting.

    Also you forgot to await f() in your run function, or at least return it to keep promise chain intact.

    var failA = true
    var failB = true
    
    const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
    
    // In reality this is Excel.run, and it injects a context 
    async function run(f) {
        await f()
    }
    
    // Simulate a failing async call
    async function fail(message, delay) {
        await sleep(delay);
        throw new Error(message)
    }
    
    // Simulate a successful async call
    async function success(message, delay) {
        await sleep(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; }
    Login or Signup to reply.
  2. 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();
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search