How to resolve a list of dynamically created Promises? - javascript

I am writing a git pre-commit hook and I want to be able to pass it an array of commands to execute, for it to execute them, and if any fail throw an error. Examples of these commands might be to run a test suite or a build.
I am having problems dynamically doing this using the promisified version of Node's child_process exec command.
So far I have a config file with 2 example commands:
config.js
const config = {
onPreCommit: ['git --version', 'node -v'],
};
module.exports = config;
If I pass in the values manually with this code I get the promise objects fulfilled with the correct values from the commands as I'd expect:
pre-commit hook
function preCommit() {
if (config.onPreCommit && config.onPreCommit.length > 0) {
Promise.allSettled([
exec(config.onPreCommit[0]),
exec(config.onPreCommit[1]),
]).then((results) => results.forEach((result) => console.log(result)));
}
}
preCommit();
However, if I try and do this dynamically like below, this throws an error:
function preCommit() {
if (config.onPreCommit && config.onPreCommit.length > 0) {
const cmdPromises = config.onPreCommit.map((cmd, i) => {
return new Promise((resolve, reject) => {
exec(cmd[i])
.then((res) => {
resolve(res);
})
.catch((err) => {
reject(err);
});
});
});
Promise.allSettled(cmdPromises).then((results) =>
results.forEach((result) => console.log(result))
);
}
}
preCommit();
Promises rejected with:
Error: Command failed: o
'o' is not recognized as an internal or external command,
operable program or batch file.
and
Error: Command failed: o
'o' is not recognized as an internal or external command,
operable program or batch file.

Thanks to the comment by mtkopone, the issue was in my map function.
Fixed by changing exec(cmd[i]) to exec(cmd)
Also updated function so hook works as intended:
function preCommit() {
if (config.onPreCommit && config.onPreCommit.length > 0) {
// Loop through scripts passed in and return a promise that resolves when they're done
const cmdPromises = config.onPreCommit.map((cmd) => {
return new Promise((resolve, reject) => {
exec(cmd)
.then((res) => {
resolve(res);
})
.catch((err) => {
reject(err);
});
});
});
// Make sure all scripts been run, fail with error if any promises rejected
Promise.allSettled(cmdPromises)
.then((results) =>
results.forEach((result) => {
if (result.status === 'rejected') {
console.log(result.reason);
process.exit(1);
}
})
)
.then(() => {
// If no errors, exit with no errors - commit continues
process.exit(0);
});
}
}
preCommit();

Related

chaining promises in functions

I have a small problem, how to create a promise chain in a sensible way so that the makeZip function will first add all the necessary files, then create the zip, and finally delete the previously added files? (The makeZip function also has to return a promise). In the example below I don't call deleteFile anywhere because I don't know exactly where to call it. when I tried to call it inside the add file function to delete the file immediately after adding it, for some unknown reason the console displayed the zip maked! log first and then file deleted.
const deleteFile = (file, result) => {
new Promise((resolve, reject) => {
fs.unlink(`./screenshots/${file}`, (err) => {
if (err) return reject(err);
console.log(`${file} deleted!`);
return resolve();
});
});
};
const addFile = (file) => {
new Promise((resolve, reject) => {
try {
zip.addLocalFile(`./screenshots/${file}`);
console.log(`${file} added`);
return resolve();
} catch {
return reject(new Error("failed to add file"));
}
});
};
const makeZip = () => {
Promise.all(fs.readdirSync("./screenshots").map((file) => addFile(file)))
.then(() => {
return new Promise((resolve, reject) => {
try {
zip.writeZip(`./zip_files/supername.zip`);
console.log("zip maked!");
resolve();
} catch {
return reject(new Error("failed making zip"));
}
});
})
.catch((err) => console.log(err));
};
the main cause of this is that you are not returning the promises you are instantiating in your function calls. Also I have some cool suggestion to make that can improve you code cleanliness.
[TIP]: Ever checked the promisify function in NodeJS util package, it comes with node and it is very convenient for converting functions that require callbacks as arguments into promise returning functions., I will demonstrate below anyhow.
// so I will work with one function because the problem resonates with the rest, so
// let us look at the add file function.
// so let us get the promisify function first
const promisify = require('util').promisify;
const addFile = (file) => {
// if addLocalFile is async then you can just return it
return zip.addLocalFile(`./screenshots/${file}`);
};
// okay so here is the promisify example, realized it wasn't applicable int the function
// above
const deleteFile = (file, result) => {
// so we will return here a. So because the function fs.unlink, takes a second arg that
// is a callback we can use promisify to convert the function into a promise
// returning function.
return promisify(fs.unlink)(`./screenshots/${file}`);
// so from there you can do your error handling.
};
So now let us put it all together in your last function, that is, makeZip
const makeZip = () => {
// good call on this, very interesting.
Promise.all(fs.readdirSync("./screenshots").map((file) => addFile(file)))
.then(() => {
return zip.writeZip(`./zip_files/supername.zip`);
})
.then(() => {
//... in here you can then unlink your files.
});
.catch((err) => console.log(err));
};
Everything should be good with these suggestions, hope it works out...
Thank you all for the hints, the solution turned out to be much simpler, just use the fs.unlinkSync method instead of the asynchronous fs.unlink.
const deleteFile = (file) => {
try {
fs.unlinkSync(`./screenshots/${file}`);
console.log(`${file} removed`);
} catch (err) {
console.error(err);
}
};
const addFile = (file) => {
try {
zip.addLocalFile(`./screenshots/${file}`);
console.log(`${file} added`);
deleteFile(file);
} catch (err) {
console.error(err);
}
};
const makeZip = () => {
fs.readdirSync("./screenshots").map((file) => addFile(file));
zip.writeZip(`./zip_files/supername.zip`);
console.log("zip maked!");
};

Order of script execution with Promise

I know that this question is almost the same as this one: Execution order of Promises but can someone explain to me where is my mistake?
I have the next functions:
// The main function
function startTesting() {
console.info("--- Thanks! Testing is running... ---");
checkFolderExistence(dirPath)
.then(checkFolderContent)
.then(searchForImportFolder)
.then(connectToDB)
.catch(err => console.error("*** ERROR *** " + err));
}
function checkFolderExistence(path) {
console.info('--- Checking the folder "' + path + '" existence... ---');
let promise = new Promise(function(resolve, reject) {
fs.readdir(path, (err) => {
if(err) {
console.error('*** ERROR **** The folder "C:\\For_testing" doesn\'t exist. Testing is stopped!!! ***');
} else {
console.info("--- The folder \"C:\\For_testing\" exists... ---");
resolve(path);
};
});
});
return promise;
}
function checkFolderContent(path) {
console.info('--- Checking the folder "' + path + '" content... ---');
filesArray = fs.readdirSync(path);
if(filesArray.length == 0) {
console.error('*** ERROR *** There are no any files in ' + path + '. Testing is stopped!!! ***');
} else {
console.info('--- The folder is checked. It contains the next files: ---');
for(let i = 0; i < filesArray.length; i++) {
console.info(filesArray[i]);
}
};
}
function searchForImportFolder() {
console.info('--- Searching for ".../Import" folder... ---');
fs.readdir(destFolderPath64, (err) => {
if(err) {
fs.readdir(destFolderPath32, (err) => {
if(err) {
console.error('*** ERROR *** The folder ".../Import" was not found ***');
} else {
console.info('--- The folder ".../Import" was successfully found... ---');
trueDestPath = destFolderPath32;
}
});
} else {
console.info('--- The folder "C:/Program Files (x86)/StoreLine/Office/Import" was successfully found... ---');
trueDestPath = destFolderPath64;
}
});
}
function connectToDB() {
console.info('--- Connecting to the database... ---');
let pool = new sql.ConnectionPool(config);
pool.connect()
.then(pool => {
console.info("--- Connected to the database! ---");
readDB(pool)
.then(function() {
console.info("--- All needed information from DB was successfully received ---");
})
.catch(err => console.error("*** ERROR *** " + err));
})
.catch(err => {
pool = new sql.ConnectionPool(configWithoutPassw);
pool.connect()
.then(pool => {
console.info("--- Connected to the database without the password! ---");
readDB(pool)
.then(function() {
console.info("--- All needed information from the DB was successfully received ---");
})
.catch(err => console.error("*** ERROR ***" + err));
})
.catch(err => {
console.error("*** ERROR *** Can't connect to the DB ***")
sql.close();
});
});
}
I need a strict order of execution of the functions: checkFolderContent => searchForImportFolder => connectToDB.
In fact the execution is the next: checkFolderContent is executed fully, then searchForImportFolder starts execute (I can see the line "--- Searching for ".../Import" folder... ---" in a console) but right after that connectToDB starts and the next line "--- Connecting to the database... ---" is appeared. And after that line I see "--- The folder ".../Import" was successfully found... ---" from the previous function.
What did I do wrong? I've read that in .then() function should return a promise. How can I do that?
searchForImportFolder doesn't return a promise, so the chain doesn't wait for that promise to complete. Do the same thing in searchForImportFolder that you've done in checkFolderExistence: Wrap the callback-style API in a promise.
A couple of notes:
checkFolderExistence should call reject in the error path; it doesn't currently.
Node provides a promisify function you can use to wrap callback-style API calls in promises, rather than doing it manually. Or you could use the promisify-fs npm module, or the promisify npm module that lets you promisify an entire API at once, or Node's own experimental promises API for fs.
You might want to make checkFolderContent async (again using promises) rather than using readdirSync, which holds up the main thread waiting on I/O.
If you're using any recent version of Node, you might want to switch to using async functions and the await keyword, as it lets you write your logical flow rather than writing a bunch of callbacks.
searchForImportFolder should return its result rather than setting a global.
So for instance, here are checkFolderExistence and searchForImportFolder using util.promisify (these assume searchForImportFolder should return its result, so you'll have to adjust code using it):
const { promisify } = require("util");
const readdirPromise = promisify(fs.readdir);
function checkFolderExistence(path) {
console.info('--- Checking the folder "' + path + '" existence... ---');
return readdirPromise(path)
.then(path => {
console.info("--- The folder \"C:\\For_testing\" exists... ---");
return path;
})
.catch(error => {
console.error('*** ERROR **** The folder "C:\\For_testing" doesn\'t exist. Testing is stopped!!! ***');
});
}
// ...
function searchForImportFolder() {
console.info('--- Searching for ".../Import" folder... ---');
return readdirPromise(destFolderPath64)
.then(() => {
console.info('--- The folder "C:/Program Files (x86)/StoreLine/Office/Import" was successfully found... ---');
return destFolderPath64;
})
.catch(() => readdirPromise(destFolderPath32))
.then(() => {
console.info('--- The folder ".../Import" was successfully found... ---');
return destFolderPath32;
})
.catch(error => {
console.error('*** ERROR *** The folder ".../Import" was not found ***');
throw error;
});
}
If you don't need all that logging, checkFolderExistence just becomes readdirPromise, and searchForImportFolder becomes:
Or if you don't need all that logging (presumably that was for debugging):
const { promisify } = require("util");
const readdirPromise = promisify(fs.readdir);
// ...
function searchForImportFolder() {
console.info('--- Searching for ".../Import" folder... ---');
return readdirPromise(destFolderPath64)
.then(() => {
return destFolderPath64;
})
.catch(() => readdirPromise(destFolderPath32));
}
And here they are using util.promisify and async/await:
Or using util.promisify and async/await:
const { promisify } = require("util");
const readdirPromise = promisify(fs.readdir);
// ...
async function searchForImportFolder() {
try {
await readdirPromise(destFolderPath64);
return destFolderPath64;
} catch (error) {
await readdirPromise(destFolderPath32);
return destFolderPath32;
}
}
If you want to avoid searching for the two different folders repeatedly, a simple tactic is just to remember the promise from searchForImportFolder and then use then on it any time you need that value:
const importFolderPromise = searchForImportFolder();
...then when you need it:
importFolderPromise.then(folder => {/*...*/});
...or in an async function:
const folder = await importFolderPromise;
The search will only happen once.

Jest mocks and error handling - Jest test skips the "catch" of my function

I'm creating a jest test to test if metrics were logged for the error handling of the superFetch function. My approach is creating a mock function for retryFetch and returning a Promise reject event. I expect that to go to the superFetch catch but it keeps ending up in superFetch then. What can I do to handle my errors in superFetch catch?
These are the functions:
// file: fetches.js
export function retryFetch(url) {
return new Promise((resolve, reject) => {
fetch(url).then(response => {
if (response.ok) {
resolve(response);
return;
}
throw new Error();
}).catch(error => {
createSomething(error).then(createSomething => {
reject(createSomething);
});
return;
});
});
});
export function superFetch(url, name, page) {
return retryFetch(url)
.then(response => {
return response;
}).catch(error => {
Metrics.logErrorMetric(name, page);
throw error;
});
}
My jest test:
import * as fetch from '../../src/utils/fetches';
describe('Fetch fails', () => {
beforeEach(() => {
fetch.retryFetch = jest.fn(() => Promise.reject(new Error('Error')));
});
it('error metric is logged', () => {
return fetch.superFetch('url', 'metric', 'page').then(data => {
expect(data).toEqual(null);
// received data is {"ok": true};
// why is it even going here? im expecting it to go skip this and go to catch
}).catch(error => {
// this is completely skipped. but I'm expecting this to catch an error
// received error is null, metric was not called
expect(Metrics.logErrorMetric).toHaveBeenCalled();
expect(error).toEqual('Error');
});
});
});
The problem is that you overwrite the function in the exported module but superFetch use the original one inside of the module, so the overwrite will have no effect.
You could mock fetch directly like this:
global.fetch = jest.mock(()=> Promise.reject())

Why are these .then() occurring out of order?

I've got a node application that spawns a child_process. When the child_process is finished running, I'd like to resolve a promise. The following code works, but the .then() statements occur out of order:
const storage = require('./storage');
const logging = require('./logging');
const process = require('child_process').spawn;
function convertIncomingFile(pathToFile) {
logging.info(`Converting ${pathToFile}`);
const convert = process(`cat`, [pathToFile], {});
return Promise.resolve(
convert.stdout.on('data', (data) => {
logging.info(data.toString('utf8'));
}),
convert.stderr.on('data', (err) => {
logging.error(err);
}),
convert.on('close', (code) => {
logging.info(`Conversion finished with status code ${code}`);
})
);
}
module.exports = {
convertFile: (filename) => {
storage.downloadFile(filename).
then((localFilename) => {
logging.info(`File saved to: ${localFilename}`);
}).
then(() => convertIncomingFile(`./files/${filename}`)).
then(() => {
logging.info(`Coversion of ${filename} complete.`);
}).
catch((apiErr) => {
logging.error(apiErr);
});
}
};
The output I get is:
info: File saved to: ./files/package.json
info: Converting ./files/package.json
info: Coversion of package.json complete.
info: {
<file contents>
}
info: Conversion finished with status code 0
As you can see the Conversion of package.json complete. statement occurs before the file contents are logged and the conversion status code statement. Why is this the case and how do I get the 'Conversion complete' statement to come after the 'status code' statement?
Promise.resolve means return a solved value that you give it, it's not realy async as you expected. Check https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve and https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise for more detailed info
function convertIncomingFile(pathToFile) {
logging.info(`Converting ${pathToFile}`);
const convert = process(`cat`, [pathToFile], {});
return new Promise((resolve, reject) => {
convert.stdout.on('data', (data) => {
logging.info(data.toString('utf8'));
}),
convert.stderr.on('data', (err) => {
logging.error(err);
reject()
}),
convert.on('close', (code) => {
logging.info(`Conversion finished with status code ${code}`);
resolve()
})
})
}
You have to pass the convertFile promise further to let next then know that it has to wait:
then(() => {
return convertFile(`./files/${filename}`);
})
and shorter equivalent:
then(() => convertFile(`./files/${filename}`))

Promise reject not working inside of callback

I'm writing a module that uses the Google API, but am wrapping everything that is callback based in a promise. This is the code of the problem area
file1.js
var File2 = require('file2')
var api = new File2()
api.auth().then(auth => {
api.search('example').then(res => {
...do some stuff...
})
}).catch(err => {
console.log('1') //Not being run
throw err
})
file2.js
class File2(){
auth() {
...works fine and resolves...
}
search() {
return new Promise((resolve, reject) => {
googleapi.somemethod(options, (err, res) => {
if(err) {
console.log('2') // DOES run
reject(new Error(err))
}
resolve(res.field) //Program crashes here because reject didn't actually reject
})
})
}
The call to auth is working just fine, but the call to search (and more specifically googleapi.somemethod) is failing, and err is defined. I check for err, and console.log('2') runs, but then console.log('1') in catch doesn't run, the error isn't thrown, and the program crashed on resolve(res) because res is undefined. I've tried putting the error catcher as the second argument to then instead of using catch, but that still doesn't work
api.search('example').then(res => {
...do some stuff...
}, err => {
console.log('2') // Still doesn't run
throw err
})
I'm running Node v6.2.1
You should return the promise:
var File2 = require('file2')
var api = new File2()
api.auth().then(auth => {
return api.search('example').then(res => { // return the promise
return ...
})
}).catch(err => {
console.log('1') // Not being run
throw err
})
Also, if you don't need auth inside search then you can unnest those promises:
var File2 = require('file2')
var api = new File2()
api.auth().then(auth => {
return api.search('example')
}).then(res => {
return ...
}).catch(err => {
console.log('1') //Not being run
throw err
})
calling reject() does not stop your program, all codes below will be executed too.
Please update from
if(err) {
console.log('2') // DOES run
reject(new Error(err))
}
resolve(res.field) //Program crashes here because reject didn't actually reject
to
if(err) {
console.log('2') // DOES run
reject(new Error(err))
}
else {
resolve(res.field) //Program crashes here because reject didn't actually reject
}
* update *
or you can shorten your code to
if(err) {
console.log('2') // DOES run
return reject(err) // no need to new Error object
}
resolve(res.field) //Program crashes here because reject didn't actually reject

Categories

Resources