Difference between callback and promise when using `net.connect` - javascript

I've built a simple script to detect if a port is opened/closed on my machine. I've successfully make it work using the old-fashion callback way, but can't make it work with Promise (async/await).
Any clue why the two scripts below are not working exactly the same? Using callbacks, it works neat. Using Promise (async/await) it crash by throwing an "uncaughtException" error.
✅ Using callbacks
const net = require('node:net')
/**
* Connect to port with callback
*/
function connectToPort(port, callback) {
const client = net.connect(port, 'localhost', function () {
callback(true)
})
client.on('error', function (err) {
callback(err)
})
}
/**
* Connect
*/
async function test(port) {
console.log(`Trying to connect to port "${port}"...`)
// Connect with callback
connectToPort(port, function (result) {
console.log({ port, open: result === true })
})
}
// Output:
// Trying to connect to port "4242"...
// { port: 4242, open: false }
test(4242)
❌ Using Promise (async/await)
const net = require('node:net')
/**
* Connect to port with Promise
*/
function asyncConnectToPort(port) {
return new Promise(function (resolve, reject) {
const client = net.connect(port, 'localhost', function () {
resolve(true)
})
client.on('error', function (err) {
reject(err)
})
})
}
/**
* Connect
*/
async function test(port) {
console.log(`Trying to connect to port "${port}"...`)
// Connect with promise
const result = await asyncConnectToPort(port)
console.log({ port, open: result === true })
}
// Output:
// Trying to connect to port "4242"...
// Error: connect ECONNREFUSED 127.0.0.1:4242
test(4242)
Both scripts look exactly the same to me. Apparently, the "error" event must be present to avoid Nodejs to throw an "uncaughtException". That "special" event is detected when using callback, but I suspect it's not with Promise. Could it be something behind the scene that differs when working with await/async script?

An error event is raised in both your code examples.
In the Promise code that you wrote you pass that error event to reject().
When you call reject you raise an exception.
Hence, you get an exception in your Promise based code but not in the other code. You added one!.
Handling it is a case of:
try {
const result = await asyncConnectToPort(port)
console.log({ port, open: result === true })
} catch (e) {
// do something with e here
}
However a Promise can only be settled once.
The code you've written will call resolve when the callback to net.connect runs and it will call reject on an error event but these are not mutually exclusive.
It's possible for your code to end up calling both resolve and reject, and also for it to call reject multiple times.

Related

Https callable cloud function not returning value

I have a Flutter app and I'm trying to get a client nonce from braintree. Per the braintree documentation, I have this in my cloud function:
exports.getClientNonce = functions.https.onCall(async (data, context) => {
gateway.clientToken.generate({}, function (err, response) {
if (err) {
throw new functions.https.HttpsError('unknown', 'Error getting client nonce');
} else {
console.log(`token: ${response.clientToken}`);
return response.clientToken;
}
});
});
Then, in my Flutter app I call the function (again, I'm following what the plugin says):
try {
HttpsCallable callable = CloudFunctions.instance.getHttpsCallable(
functionName: 'getClientNonce',
);
dynamic result = await callable.call({});
final value = result.data;
debugPrint('token: $value');
var data = await BraintreePayment().showDropIn(
nonce: value,
amount: '2.0',
enableGooglePay: false,
inSandbox: true);
print("Response of the payment $data");
} on CloudFunctionsException catch (e) {
debugPrint('An error occurred');
} catch (e) {
debugPrint('An error occurred');
}
}
I tried changing the cloud function so that it only returns a random number (as soon as the function is executed), and my Flutter app is correctly receiving the value (so the cloud function is communicating fine). And in my Firebase console, I am able to view the client nonce specified by console.log. But the function is for whatever reason unable to return the actual client nonce. (It should be should be some string hash that is >2000 characters long)
The callable function needs to return a promise from the top-level of the function callback that resolves with the value to return. Right now, you're returning nothing from the top-level. The return you have now is just returning a value from the inner callback function that you pass to braintree API. This isn't going to propagate to the top level.
What you need to do is either use a version of braintree API that returns an API (if one exists), or promisify the existing call that uses a callback.
See also "3. Node style callback" here: How do I convert an existing callback API to promises?
I have not tested this, but the general format if you apply that pattern will look more like this:
exports.getClientNonce = functions.https.onCall(async (data, context) => {
return new Promise((resolve, reject) => {
gateway.clientToken.generate({}, function (err, response) {
if (err) {
reject(new functions.https.HttpsError('unknown', 'Error getting client nonce'));
} else {
console.log(`token: ${response.clientToken}`);
resolve(response.clientToken);
}
});
});
});

How to handle error properly with nested promises in different modules

I'm new to Node JS, and I'm struggling to handle error properly when using promises.
Here is what I currently have:
I have a module call db.js with an init function used to set up and start the db connection:
function initDb(){
return new Promise((resolve, reject) => {
if(database){
console.warn("Trying to init DB again !")
}else{
client.connect(config.db.url, {useNewUrlParser: true, useUnifiedTopology: true})
.then((client) => {
console.log("DB initialized - connected to database: " + config.db.name)
database = client.db(config.db.name)
})
.catch((err) => {
reject(err)
})
}
resolve(database)
})}
This function will return a promise, and it will be called into the index.js:
initDb()
.then(() => {
app.listen(8081, function () {
console.log('App is running on port 8081')
})
})
.catch((error) => {
console.log(error)
})
As you can see I have two catch. One in the db module and the other one in the index.js
It seams weird to catch error in two places... What is the good pattern to use to handle error in this case ?
You'll want to avoid the Promise constructor antipattern. Also don't store the database itself in a variable, store the promise for it instead.
let databasePromise;
function initDb() {
if (databasePromise) {
console.debug("DB already initialised");
return databasePromise;
}
databasePromise = client.connect(config.db.url, {useNewUrlParser: true, useUnifiedTopology: true})
.then((client) => {
console.log("DB initialized - connected to database: " + config.db.name)
return client.db(config.db.name)
});
return databasePromise;
}
If you don't write catch in the initDb function in db.js module, you can catch that error in the calling function, so it's okay if you don't write .catch((err) => { reject(err)}) in db.js module, the error will go to calling function in index.js and you can directly handle it there.
It is not weird to catch the error at both the place, in fact in coming versions of node this will be a recommended practice to write catch(err) for handling errors at all promises.

Why am I unable to catch discord.js promise rejections in event callbacks?

So I'm making a discord bot. For simplicity's sake, here is a very small portion that illustrates my problem:
const Discord = require('discord.js');
const client = new Discord.Client();
client.on('ready', async () => {
throw new Error('Omg');
});
async function start() {
try {
await client.login(process.env.DISCORD_BOT_TOKEN);
} catch (err) {
console.error('Caught the promise rejections');
}
}
start();
When I run this code, I expect the output to be Caught the promise rejections and the process should subsequently exit. However this is not the case. Instead I get a PromiseRejectionWarning and the process does not exit (I have to press Ctrl-C to do so). I first thought that maybe errors in callbacks don't get propagated to code that calls them, so I made another pure JS example:
const client = {
on(event, callback) {
this.callback = callback;
},
async login(token) {
while (true) {
// I assume the login method calls the callback in D.js
// (where else could it be called?)
await this.callback();
await sleep(5000);
}
},
};
client.on('ready', async () => {
throw new Error('Omg');
});
async function start() {
try {
await client.login(process.env.DISCORD_BOT_TOKEN);
} catch (err) {
console.error('Caught the promise rejections');
}
}
start();
However in this case, the output is exactly as expected; I see the line from the catch and the process immediately exits. Without the catch I get the unhandled promise rejection errors and an unfinished process.
So my question: Why am I unable to catch promise rejections in my event callbacks (like on('ready'))?
The reason is, because your second code, is not how discord event emitter works, nor Node.js built in EventEmiter does.
The callback function for the ready event is not executed with an await, and it doesn't have a .catch handler attached to it, that's why you get an UnhandledPromiseRejectionWarning.
When using async in an EventEmitter callback, you should handle the error, if you don't you'll get the warning, because no other code is handling it.
client.on('ready', async () => {
try {
throw new Error('Omg');
} catch(e) {
}
});
In your specific case, it seems that you want to trigger an error if some condition is met on 'ready'. So what you should do instead, is wrap that listener in a Promise.
function discordReady(client) {
return new Promise((resolve, reject) => {
client.once('ready', async () => {
reject(new Error('Omg'));
// resolve..
});
})
}
async function start() {
try {
await Promise.all([
discordReady(client),
client.login(process.env.DISCORD_BOT_TOKEN),
]);
} catch (err) {
console.error('Caught the promise rejections');
}
}
That will get you the expected behaviour

Sinon crypto stub for method within a callback

I'm trying to test a simple function that generates a random name using the nodejs crypto library. I'm using sinon to stub out a method call within the callback of pseudoRandomBytes but the stub doesn't seem to be called. Example:
getFileName.js
const crypto = require('crypto');
module.exports = (req, file, cb) => {
crypto.pseudoRandomBytes(32, (err, raw) => {
try{
cb(err, err ? undefined : crypto.createHash('MD5').update(raw).digest('hex'));
} catch(err) {
cb(err);
}
});
};
Test (running in mocha)
it('Crypto Error: createHash', function () {
const crypto = require('crypto');
const expectedError = new Error('stub error occurred');
let cryptoStub = sinon.stub(crypto, 'createHash').throws(expectedError);
let callback = sinon.spy();
getFileName(null, null, callback);
cryptoStub.restore();
sinon.assert.calledWith(callback, expectedError);
});
I would expect the above test to throw once createHash gets called. If I move the crypto.createHash call outside of the callback (before the pseudoRandomNumber call) it works just fine. I a bit of a newbie so my basic understanding of what sinon and nodejs are doing could be completely wrong. Any help would be much appreciated.
The reason why it seems like createHash() wasn't called was because you were making an assertion before the callback call was complete due to asynchronous function.
Promise with async/await will work. Another method which doesn't involve changing your module to use promise is to do your assertions within the callback.
it('Crypto Error: createHash', function (done) {
const crypto = require('crypto');
const expectedError = new Error('stub error occurred');
let cryptoStub = sinon.stub(crypto, 'createHash').throws(expectedError);
getFileName(null, null, function (err, hash) {
sinon.assert.match(err, expectedError);
cryptoStub.restore();
done();
});
});
This way, you can check that the callback is called with the expected error. One way to confirm this is you can change line 4 to .throws('some other error') and the test will fail.
The problem is that crypto.pseudoRandomBytes() is an async function, so the rest of your test code executes before your callback. That way, your stub is restored before your function is actually used it.
In order to make it properly work, you should update your getFileName.js so it returns a promise - that way you can await it
module.exports = (req, file, cb) => {
return new Promise((resolve, reject) => {
crypto.pseudoRandomBytes(32, (err, raw) => {
try{
cb(err, err ? undefined : crypto.createHash('MD5').update(raw).digest('hex'));
resolve();
} catch(err) {
reject(cb(err));
}
});
});
};
and then in your test
// added async
it('Crypto Error: createHash', async () => {
const crypto = require('crypto');
const expectedError = new Error('stub error occurred');
let cryptoStub = sinon.stub(crypto, 'createHash').throws(expectedError);
let callback = sinon.spy();
await getFileName(null, null, callback);
// once we are here, the callback has already been executed and the promise that getFileName resolved.
cryptoStub.restore();
sinon.assert.calledWith(callback, expectedError);
});

Process exited before completing request - AWS Lambdas

I'm using AWS Lambdas to try and connect to a CMS and i've encountered the following error:
Process exited before completing request
Here's my snippet below:
require('dotenv').config({ silent: true });
const contentful = require('contentful');
exports.handler = (event, context) => {
const client = contentful.createClient({
space: process.env.SPACE_ID,
accessToken: process.env.CDA_TOKEN
})
client.getEntries({
'content_type': 'thumbnail'
})
.then(function (entries) {
context.succeed(JSON.stringify(entries));
})
};
Does this error suggest that i'm getting an error somewhere in my code that's preventing it from running context.succeed or that i'm using context.succeed incorrectly?
Process exited before completing request
It means that you got an unhandled exception. Your handler basically crashed without telling Lambda why.
Looking at your code, it is most likely that the client.getEntries() promise got rejected and you didn't provide a .catch() to your promise.
You can do the following instead...
// Use callback coz context.succeed() is soooo legacy.
exports.handler = (event, context, callback) => {
const client = contentful.createClient({
space: process.env.SPACE_ID,
accessToken: process.env.CDA_TOKEN
})
return client.getEntries({
'content_type': 'thumbnail'
})
// Be consistent with arrow function usage.
.then((entries) => callback(null, JSON.stringify(entries)))
// This is what is missing.
.catch((err) => {
// Log the error so you know what it is and fix it.
console.error(err);
// Be polite and tell Lambda that the invocation failed.
callback(err);
});
};

Categories

Resources