UltrasoundJelly UltrasoundJelly - 2 months ago 24
Node.js Question

can't catch Promise queue errors

I'm setting up a node app where I have to run a series of asynchronous spawn tasks in order, so I've got a queue system set up. It runs well when it reaches no errors. However, it's failing to catch the errors and log them when they do happen. Here's where I am right now:

function queue(tasks) {
let index = 0;
const runTask = (arg) => {
if (index >= tasks.length) {
return Promise.resolve(arg);
}
return new Promise((resolve, reject) => {
tasks[index++](arg).then(arg => resolve(runTask(arg))).catch(reject);
});
}
return runTask();
}

function customSpawn(command, args) {
return () => new Promise((resolve, reject) => {
const child = spawn(command, args, {windowsVerbatimArguments: true});
child.on('close', code => {
if (code === 0) {
resolve();
} else {
reject();
}
});
});
}


The queue is built and executed like this:

myqueue.push(customSpawn('cmd.exe', ['/c', convertpath, filelist[i], '-interlace', 'line', '-chop', croppixels, '-resize', '300', outfile]));
queue(myqueue).then(([cmd, args]) => {
console.log(cmd + ' finished - all finished');
}).catch(function(error) {
console.error(error.stack);
});


The following error is thrown:
Uncaught (in promise) TypeError: undefined is not a function(…)

Answer

Sorry, I can't quite understand your queue function so I refactored it using Array.prototype.reduce. This is a standard way to chain things in a compact way. The following module models your case and will run in node. It demonstrates how the error handling works. I assume that you want to abort the whole chain if there is an error?

'use strict';
const insp = require('util').inspect;

const size = 10;
const throwAt = 6;

var template = [1,2,3,4,5,6,7,8,9,];
var cmds = template.map((_, i) => ({cmd: `cmd ${_}`, args: [`arg1-${i}`, `arg2-${i}`]}));
var queue = [];

// promise factory
function makePromise (command, args) {
  return () => new Promise((resolve, reject) => {
    setTimeout(_ => {if(command.indexOf(throwAt) > 0) {
      return reject(command);  // whatever is passed here will hit the catch
    }
    console.log(`${command}\t${insp(args)}`);
    resolve()
    }, Math.random() * 1000)
  })
}

// populate the queue
cmds.forEach(c => queue.push(makePromise(c.cmd, c.args)));
// then execute it and catch all errors
queue.reduce((q, p) => q.then(p), Promise.resolve())
  .catch(e => console.log(`error: ${e}`));

You can also add some re-try logic like this...

// promise factory
function makePromise (command, args) {
  return () => new Promise((resolve, reject) => {
    setTimeout(_ => {
      if(command.indexOf(throwAt) > 0 && command.indexOf('retry') === -1) {
        return makePromise(command + 'retry', args)()
          .then(_ => resolve(), e => reject(e));
      }
      console.log(`${command}\t${insp(args)}`);
      resolve()
    }, Math.random() * 1000)
  })
}

Error propagation

Playing around with this, I noticed that any error thrown inside a a resolve or a reject callback will result in the reject of the calling block being called, passing the error object as the single argument. This means that the error can be thrown rather than just calling reject. This has the benefit of delivering a stack trace also.

Managing the cascade

By adding a reject callback to each promise in the chain, the error propagation can be managed if needed. If a reject calback is added however, it must re-throw the error if it needs to be propagated.

Here is a an implementation of these two principles...

function makePromise2 (command, args) {
  return (retry) => new Promise((resolve, reject) => {
    if(retry){
      console.log(`throw at ${command}`);
      throw new Error(`sorry, tried twice!`);
    }
    setTimeout(_ => {
      if(command.indexOf(throwAt) > 0) {
/*
        if(retry)   // throwing here will not be handled
          throw new Error(`sorry, tried my best!`);
*/
        return makePromise2(command, args)(true)
          .then(resolve, reject);  // without this it will fail silently
      }
      console.log(`${command}\t${insp(args)}`);
      resolve();
    }, Math.random() * 1000)
  })
}

function Reject (cmd) {
  return function reject (e) {
  console.log(`re-throw at ${cmd.cmd}`);
  throw e;  // if you have a reject callback then you must propagate the error
}}

// populate the queue
cmds.forEach(c => queue.push(makePromise2(c.cmd, c.args)));
// then execute it and catch all errors
// the Reject gives the opportunity to manage the error cascade
queue.reduce((q, p, i) => q.then(p, Reject(cmds[i])), Promise.resolve())
  .catch(e => console.log(`catch...\n${e.stack}`));
Comments