Discord.js : Variable is undefined despite using await - javascript

I'm making a discord bot using discord.js that listens to slash commands in a discord server and manipulates data on a google sheet. I've come across a frustrating problem while trying to make a confirmation system for a command. The issue is with the getConfirmation() method. Here is the relevant code:
async execute(interaction) {
try {
await interaction.deferReply();
// get player from spreadsheet
const iss = interaction.options.getInteger("iss");
const player = await getPlayerByISS(iss);
// get confirmation
let hasConfirmed = await getConfirmation(interaction, player);
if (!hasConfirmed) {
await interaction.editReply("Draft choice abandoned.");
return;
}
// if confirmed, draft the player
let draftedPlayer = await draftPlayer(interaction.user.tag, player);
await interaction.editReply(draftedPlayer);
} catch (error) {
console.log(error);
await interaction.editReply("Could not draft player");
}
}
and
async function getConfirmation(interaction, player) {
try {
// send confirmation msg
const message = await interaction.channel.send({
content: `Are you sure you want to draft ${formatPlayerString(
player
)}?`
});
const filter = (reaction, user) => {
return (
["👎", "👍"].includes(reaction.emoji.name) &&
user.id === interaction.user.id
);
};
// react to the msg to allow for easier reacting
await Promise.all([message.react("👍"), message.react("👎")]);
// awaitReactions just returns a list of all reaction objs that match above
// filter
let didConfirm, responseMessage;
message
.awaitReactions({
filter,
max: 1,
time: 60000,
errors: ["time"],
})
.then((collected) => {
const reaction = collected.first();
didConfirm = reaction.emoji.name === "👍";
responseMessage = didConfirm
? "Confirmed. Drafting now..."
: "Cancelled confirmation.";
})
.catch((collected) => {
console.log(collected);
responseMessage =
"Did not receive response in time. Abandoning...";
didConfirm = false;
})
.finally(async (collected) => {
// clean up after 3s to prevent clutter
setTimeout(() => message.delete(), 3000);
// resolve the promise
await message.edit(responseMessage);
return didConfirm;
});
} catch (error) {
console.log("Errored out : ", JSON.stringify(error));
console.log(error);
throw error;
}
}
Sorry for the large chunk of code but let me explain. I want the bot to send a confirmation message, listen for any reaction replies to the message, and then proceed with drafting the player if the user who sent the /draft command reacts with a thumbs up emoji, and abandons otherwise.
The issue is with the getConfirmation() method. Despite me having designated getConfirmation() as an async method, which should wrap my response in a promise and awaiting it, the code in execute() continues to run immediately after the await getConfirmation() line. It sets hasConfirmed = undefined and proceeds with that assumption. So no matter what the user reacts, the code will reach the !hasConfirmed block, and respond with "Draft choice abandoned".
However, this does not make any sense to me. Since getConfirmation() is an async method, and we are awaiting it in another async method, shouldn't the await keyword halt execution until the promise has been fulfilled and hasConfirmed has a value? The confirmation message sent in getConfirmation does get correctly updated when the user reacts though, so I know that that code is being reached. Am I missing something obvious? Do I just fundamentally misunderstand asynchronous programming?
I'd really appreciate any help! Thanks a lot.
Edit: Phil's answer was it

getConfirmation() doesn't return anything so will always resolve to undefined. FYI the return value from finally() is not used to resolve the promise chain
// Quick demo showing what happens with finally
Promise.resolve(undefined).finally(() => "finally").then(console.log);
It's also best not to mix async / await with .then() / .catch() as it can quickly get confusing.
I would instead structure the await reactions part of your code like this
let didConfirm, responseMessage;
try {
const collected = await message.awaitReactions({
filter,
max: 1,
time: 60000,
errors: ["time"],
});
const reaction = collected.first();
didConfirm = reaction.emoji.name === "👍";
responseMessage = didConfirm
? "Confirmed. Drafting now..."
: "Cancelled confirmation.";
} catch (err) {
console.warn(err);
responseMessage = "Did not receive response in time. Abandoning...";
didConfirm = false;
}
// Things after an async try..catch is similar to finally
// only you can actually return a value
// clean up after 3s to prevent clutter
setTimeout(() => message.delete(), 3000);
// resolve the promise
await message.edit(responseMessage);
return didConfirm;

Related

Execute code synchronously in node.js controller

Just started with Node.js few weeks ago, i'm trying to execute this code synchronously located in my controller
Quest.js
async function create(req, res, next) {
const formValue = req.body['form'];
let quest;
const riddles = [];
try {
//get current user who created this Quest
await User.findOne({_id: req.body.id})
.then(currentUser => {
/*-------------------------------START QUEST CREATION-------------------------------*/
quest = new Quest({
admin: currentUser,
hunter: new User,
launchDate: formValue.launchDate,
penaltyTime: formValue.penaltyTime,
});
/*-------------------------------END QUEST CREATION-------------------------------*/
/*-------------------------------START RIDDLES CREATION-------------------------------*/
let riddle;
console.log('step1');
//Riddles instantiation
for (const [key, participantEmail] of Object.entries(formValue.participantsEmail)) {
if (formValue.riddle.text == "") {
throw ('errorMsg:IncorrectRiddleText/code:422/field:riddleText');
}
if (formValue.riddle.answer == "") {
throw ('errorMsg:IncorrectRiddleAnswer/code:422/field:riddleAnswer');
}
//if its the first riddle => i.e : the one created by the current user
if (`${key}` == 0) {
riddle = new Riddle({
quest: quest,
author: currentUser,
authorEmail: currentUser.email,
text: formValue.riddle.text,
answer: formValue.riddle.answer,
status: true,
nextRiddle: null
});
}
//if it's a waiting riddle
else {
if (!validator.validateEmail(participantEmail.participantEmail)) {
throw ('errorMsg:IncorrectEmail/code:422/field:participantEmail');
}
if (participantEmail.participantEmail)
riddle = new Riddle({
quest: quest,
authorEmail: `${participantEmail.participantEmail}`,
nextRiddle: null
});
//assign last created riddle to the one before to make the good order
riddles[`${key}` - 1].nextRiddle = riddle;
}
//create riddle list for Quest
riddles.push(riddle);
}
/*-------------------------------END RIDDLES CREATION-------------------------------*/
/*-------------------------------START USER MANAGEMENT-------------------------------*/
//create a User for the hunter or if he already have an account => link it to the new Quest
User.findOne({email: formValue.hunterEmail})
.then(hunterUser => {
console.log('step2');
if (hunterUser == null) {//if userHunter doesn't exist yet => create it
userHelper.createUser(
formValue.hunterFirstName,
formValue.hunterLastName,
formValue.hunterFirstName + formValue.hunterLastName.slice(-1),
formValue.hunterEmail,
Text.generateRandomPassword()
).then((createdUser) => {
console.log('step3');
hunterUser = createdUser;
}).catch(error => {
console.log('error1')
error = Text.parseErrorToObject(error)
return res.status(parseInt(error.code.toString())).json(error);
}
);
}
console.log('step4');
questHelper.saveQuest(quest, riddles, hunterUser)
.then(() => {
console.log('step5');
return res.status(200).json({'msg': 'Quest created'})
})
.catch(error => {
console.log('error2')
error = Text.parseErrorToObject(error)
return res.status(parseInt(error.code.toString())).json(error);
}
);
}
).then().catch(error => {
throw (Text.parseErrorToObject(error));
})
/*-------------------------------END USER MANAGEMENT-------------------------------*/
})
.catch(error => {
throw (Text.parseErrorToObject(error));
}
);
} catch (e) {
console.log('error3')
return res.status(parseInt(e.code.toString())).json(e);
}
};
But when i execute this if i display the logs ihave this :
step1
step2
step4
step3
I know why it's doing this, du to the fact that JS is asynchronous multi-thtreading.
But i can't figure out how to execute this block (step4) :
questHelper.saveQuest(quest, riddles, hunterUser)
.then(() => {
console.log('step5');
return res.status(200).json({'msg': 'Quest created'})
})
.catch(error => {
console.log('error2')
error = Text.parseErrorToObject(error)
return res.status(parseInt(error.code.toString())).json(error);
}
);
When the previous block is over. That mean, when hunterUser = createdUser; is executed
EDIT :
Thanks to all answers, i have cleaned my code and went out of all the then/catch block.
It lost a lot of weight :p and so much readable.
The error management is so much easier this way too
async function create(req, res, next) {
const formValue = req.body['form'];
const riddles = [];
try {
//get current user who created this Quest
const currentUser = await User.findOne({_id: req.body.id});
/*-------------------------------START QUEST CREATION-------------------------------*/
let quest = await new Quest({
admin: currentUser,
hunter: new User,
launchDate: formValue.launchDate,
penaltyTime: formValue.penaltyTime,
});
/*-------------------------------END QUEST CREATION-------------------------------*/
/*-------------------------------START RIDDLES CREATION-------------------------------*/
let riddle;
//Riddles instantiation
for (const [key, participantEmail] of Object.entries(formValue.participantsEmail)) {
if (formValue.riddle.text == "") {
throw ('errorMsg:IncorrectRiddleText/code:422/field:riddleText');
}
if (formValue.riddle.answer == "") {
throw ('errorMsg:IncorrectRiddleAnswer/code:422/field:riddleAnswer');
}
//if its the first riddle => i.e : the one created by the current user
if (`${key}` == 0) {
riddle = new Riddle({
quest: quest,
author: currentUser,
authorEmail: currentUser.email,
text: formValue.riddle.text,
answer: formValue.riddle.answer,
status: true,
nextRiddle: null
});
}
//if it's a waiting riddle
else {
if (!validator.validateEmail(participantEmail.participantEmail)) {
throw ('errorMsg:IncorrectEmail/code:422/field:participantEmail');
}
if (participantEmail.participantEmail)
riddle = new Riddle({
quest: quest,
authorEmail: `${participantEmail.participantEmail}`,
nextRiddle: null
});
//assign last created riddle to the one before to make the good order
riddles[`${key}` - 1].nextRiddle = riddle;
}
//create riddle list for Quest
riddles.push(riddle);
}
/*-------------------------------END RIDDLES CREATION-------------------------------*/
/*-------------------------------START USER MANAGEMENT-------------------------------*/
//create a User for the hunter or if he already have an account => link it to the new Quest
let hunterUser = await User.findOne({email: formValue.hunterEmail});
if (hunterUser == null) {//if userHunter doesn't exist yet => create it
hunterUser = await userHelper.createUser(
formValue.hunterFirstName,
formValue.hunterLastName,
formValue.hunterFirstName + formValue.hunterLastName.slice(-1),//TODO add a unique ID
formValue.hunterEmail,
Text.generateRandomPassword()
)
}
await questHelper.saveQuest(quest, riddles, hunterUser)
return res.status(200).json({'msg': 'Quest created'})
/*-------------------------------END USER MANAGEMENT-------------------------------*/
} catch (error) {
error = Text.parseErrorToObject(error);
return res.status(parseInt(error.code.toString())).json(error);
}
};
clearly you already understand how "async" and "await" work. Now it's just about remembering to apply them for every network I/O operation.
And so if you change
await User.findOne({_id: req.body.id})
.then(currentUser => {
to
await User.findOne({_id: req.body.id})
.then(async currentUser => {
then suddenly you'll be able to use await inside that block.
Now you need to change
User.findOne({email: formValue.hunterEmail})
.then(hunterUser => {
to
await User.findOne({email: formValue.hunterEmail})
.then(hunterUser => {
your current solution basically says "find user and once you're done do this and that..." and then you don't actually wait until the user is found, so that basically has to be fixed.
Better yet, you could drop all of the '.then()' and '.catch' blocks since they can be easily replaced with the "async/await" syntax.
So instead of writing
await User.findOne({email: formValue.hunterEmail})
.then(hunterUser => { ...someLogic })
in a more modern way you could write
const hunerUser = await User.findOne({email: formValue.hunterEmail});
//...someLogic
First, you're using await and .then() at the same line of code, which won't produce any value.
I'd suggest using the ES6 async await keywords alone which will make your different parts of code execeute after each promise has been resolved.
Step 4 should look something like this:
try {
const result = await questHelper.saveQuest(quest, riddles, hunterUser);
console.log('step5');
return res.status(200).json({'msg': 'Quest created'})
}
catch (error) {
console.log('error2')
error = Text.parseErrorToObject(error)
return res.status(parseInt(error.code.toString())).json(error);
}
Any logic that should happen after the asynchronous operation should be moved to its callback function. Currently you have this structure:
User.findOne({email: formValue.hunterEmail})
.then(hunterUser => {
//...
userHelper.createUser(/*...*/)
.then((createdUser) => {
//...
// YOU WANT TO DO SOMETHING HERE
});
// THIS IS THE CODE YOU WANT DO EXECUTE ABOVE
});
If you want that later block of code to be executed in response to the completion of the userHelper.createUser operation, move it to the callback where you respond to the result of that operation:
User.findOne({email: formValue.hunterEmail})
.then(hunterUser => {
//...
userHelper.createUser(/*...*/)
.then((createdUser) => {
//...
// THIS IS THE CODE YOU WANT DO EXECUTE
});
});
Overall it seems you're getting confused by a large and complex set of nested callbacks. The async and await syntax for Promises is meant to address that and make the syntax more clear. If these asynchronous operations return Promises then you're definitely encouraged to make use of that syntax instead of all of these .then() callbacks.
But even with the .then() syntax, all you need to do is identify your blocks of code and where they belong. Either something should execute immediately (after the .then()), or it should execute in response to an asynchronous operation (inside the .then().

Async function not running some code even inside await

I have a Node.js AWS Lambda function created via the serverless framework. I have multiple helper functions inside it. I am having an issue with one of them due to being async. The function runs and logs out all parts I put comments next to however it doesn't update callDuration. I think that the code is having an issue due to async where it finishes in the wrong order. My goal is to be able to return the callDuration to my main function for further processing. How can I get all code to process/run and be able to meet my goal and have the code run in the right order
Here is the function:
const callAggregate = async (billingData, billingDB) => {
const accountSid = process.env.TWILIO_ACCOUNT_SID
const authToken = process.env.TWILIO_AUTH_TOKEN
const client = require('twilio')(accountSid, authToken)
// Setup model
const Billing = billingDB.model('Billing')
await Billing.findOne({_id: billingData._id}).exec().then(bill => {
const callArray = bill.callSid
console.log(bill) // This logs out
let callDuration = 0
for (const call of callArray) {
console.log(call) // This logs out
client.calls(call)
.fetch()
.then(callDetails => {
console.log(callDetails) // This logs out
callDuration += callDetails.duration
})
}
console.log(`Billing for ${callDuration} minutes of voice calling for ${billingData._id}`) // This logs out
Billing.findOneAndUpdate(
{_id: billingData._id},
{ $inc: { call_duration: callDuration }, callSid: []},
(err, doc) => {
if(err) {
console.log(err)
}
}
)
return callDuration
})
}
This is a case of mixing and matching promises with plain callbacks and mixing await with .then(), both of which make proper flow-control and error handling management difficult.
Inside your function which is async and uses await in some places, you also have a promise you are not awaiting (which means it runs open loop and nothing waits for it) and you have a database function that is using a plain callback, not the promise interface so nothing waits for it either.
More specifically, nothing is waiting for this:
client.calls(call).fetch()
So, because of not waiting for the .fetch() to finish, you were attempting to use the variable callDuration before the code was done modifying that variable (giving you the wrong value for it).
Similarly, nothing is waiting for Billing.findOneAndUpdate(...) to complete either.
A clean solution is to switch everything over to promises and await. This involves, using only promises with your database (no plain callbacks) and converting the .then() handlers into await.
async function callAggregate(billingData, billingDB) {
const accountSid = process.env.TWILIO_ACCOUNT_SID
const authToken = process.env.TWILIO_AUTH_TOKEN
const client = require('twilio')(accountSid, authToken)
// Setup model
const Billing = billingDB.model('Billing')
let bill = await Billing.findOne({ _id: billingData._id }).exec();
const callArray = bill.callSid
console.log(bill) // This logs out
let callDuration = 0
for (const call of callArray) {
console.log(call) // This logs out
let callDetails = await client.calls(call).fetch();
console.log(callDetails) // This logs out
callDuration += callDetails.duration
}
console.log(`Billing for ${callDuration} minutes of voice calling for ${billingData._id}`) // This logs out
let doc = await Billing.findOneAndUpdate({ _id: billingData._id }, { $inc: { call_duration: callDuration }, callSid: [] }).exec();
return callDuration
}

Discord.js v13 How to prevent the bot from crashing on attempted message deletion?

I have this 'clear' command for my discord bot and it should work like this:
-user types in !mute
the bot deletes this many messages (up to 100)
the bot sends a message saying that these messages were deleted
the bot deletes that message 5 seconds later
It all works except for the last part, you see if after executing part 2 the message is deleted by another source then the bot can't find a message to delete and crashes.
The code is as follow:
module.exports = {
name: 'clear',
description: "clears messages",
async execute(message, args)
{
if(!args[0]) return message.reply("Please specify how many messages you want to clear!");
if(isNaN(args[0])) return message.reply("Please enter a number of messages you want to clear!");
if(args[0] > 100) return message.reply("You can't delete more than 100 messages!");
if(args[0] < 1) return message.reply("You can't delete less than 1 message!");
await message.channel.messages.fetch({limit: args[0]}).then(messages =>{
message.channel.bulkDelete(messages).then(() => {
message.channel.send("Deleted " + args[0] + " messages") .then(msg => {
let id = msg.id;
setTimeout(function(){
if(message.channel.messages.fetch(id))
{
try {
msg.delete()
}
catch (error) {
console.log(error)
return
}
}
}, 5000);
})
});
})
}
}
The error I'm getting is:
C:\Users\Miki\Desktop\discord boty\jajco bot\node_modules\discord.js\src\rest\RequestHandler.js:350
throw new DiscordAPIError(data, res.status, request);
^
DiscordAPIError: Unknown Message
at RequestHandler.execute (C:\Users\Miki\Desktop\discord boty\jajco bot\node_modules\discord.js\src\rest\RequestHandler.js:350:13)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async RequestHandler.push (C:\Users\Miki\Desktop\discord boty\jajco bot\node_modules\discord.js\src\rest\RequestHandler.js:51:14)
at async MessageManager._fetchId (C:\Users\Miki\Desktop\discord boty\jajco bot\node_modules\discord.js\src\managers\MessageManager.js:219:18) {
method: 'get',
path: '/channels/943105549795471443/messages/946096412922368100',
code: 10008,
httpStatus: 404,
requestData: { json: undefined, files: [] }
}
As you can see I've tried using the try...catch() but it didn't fix the problem. Any ideas on what to do? Or maybe there is a mistake in my code like a missing import or some other thing like that?
First off lets take a look at this code:
try {
msg.delete();
} catch (error) {
return console.log(error);
}
This code won't do what you expect. msg.delete(); is a function that returns a Promise. A try/catch structure won't do anything if the promise fails. So instead, to catch the error, you have to use the catch method on the Promise, just as you used .then() earlier, you'll have to use .catch().
So your new code will be like this:
msg.delete().catch(error => console.error(error);
// Or, even better:
msg.delete().catch(console.error);
That is the exact same thing here:
if (message.channel.messages.fetch(id))
That will always be true because the fetch() function will return a Promise ans since it is neither a 0, '' (empty string),NaN, false, undefined or null See here for false values
Instead, what you're trying to do is check if the messages contain your id:
message.channel.messages.fetch(id).then(msg => {
// Do what you want with the fetched msg
}).catch(error => {
// Console error why it failed to fetch the id (probably because it doesn't exist or got deleted)
message.reply('Failed to fetch the id');
});
Lastly this code also has some issues:
message.channel.bulkDelete(messages).then(() => {
messages is a Collection, not a number, bulkDelete() waits for a number so instead do this:
message.channel.bulkDelete(messages.size).then(() => {
I personnally don't see why you have to fetch the message, you can simply do this:
message.channel.messages.fetch({
limit: args[0]
}).then(messages => {
message.channel.bulkDelete(messages.size).then(() => {
message.channel.send(`Deleted ${args[0]} messages`).then(msg => {
setTimeout(() => msg.delete(), 5000);
});
});
});
All you need to do is to await msg.delete() inside your try block. A promise must be settled before it can be tested for any error.
try {
// Add 'await'
await msg.delete();
} catch (error) {
return console.log(error);
}
If you want to void the error to have the function fail silently:
msg.delete()
.catch(() => null);
You can also check Message#deletable before having your client attempt to delete it:
if (msg.deletable) {
// I'd still recommend adding your preferred error handling
msg.delete();
}
return message.reply({ content: "Done" })
.then(m => setTimeout(() => message.delete().catch(() => null), 2500))
use it like this it'll solve the issue for d.js v13.6

JS - Interrupt async execution

The actual Problem
The problem I am trying to solve is that a user may not provide enough information on a message (discord). To then get all of the necessary data the user is prompted to react to a bot message.
There are 3 stages of that bot message, "InitialPrompt", "TimeoutWarning" and "TimeoutSuccess(TicketFailure)".
What I wanted the solution to be
With the way I've written my code there is no way to abort the timeout after it has been initialized. I thought that throwing an error would stop the execution of a function. I guess that doesn't happen because async calls get queued up instead of being ran line by line.
Is there a way to do this without adding a boolean and checking infront of each function call?
The solution that I could come up with
const interPtr = {interrupted : false};
interruptSignal(interPtr);
if(interPtr.interrupted) return;
console.log("...");
...
The actual code
JS Fiddle
(async () => {
const sleep = async time => new Promise(resolve => setTimeout(resolve, time));
const interruptSignal = () => new Promise(async (res, rej) => {
console.log("interruptStarted")
await sleep(2000);
console.log("interruptRan");
throw "Interrupted"
});
const timeOutHandler = async () => {
interruptSignal();
console.log("TimeoutStarted")
await sleep(5000);
console.log("TimeoutWarning")
await sleep(5000);
console.log("TimeoutSuccess->TicketFailure")
};
try {
await timeOutHandler();
} catch (e) {
console.log(e);
}
})()

How do I return a value in a promise? (Firebase.get())

I've been struggling for the past four hours on this. I am writing a function to see if a property named "data" exists in my Firebase storage. If it does, I want to do one thing, if it doesn't I want to do something else. However, I cannot figure out how this asynchronous stuff works for the life of me. I simplified my code below. Basically I just want to wait for the data to be fetched before I reach the if/else. I've been playing around with different options but keep getting errors or some other issue. The code below is the closest I've gotten to working where the code doesn't crash but even if "data" does not exist in the Firestore, I'm always going through the else clause and I don't know why. Can someone help me figure out what I am doing wrong?
const fetchDataFromDB = async (user) => {
let query = db
.collection("users")
.doc(user)
.get()
.then((doc) => {
doc.data();
console.log(doc.data().data);
});
return await query;
};
export const getSchedule = (miles, user) => {
const firebaseData = fetchDataFromDB(user);
console.log(firebaseData);
// WAIT FOR FETCH BEFORE CONTINUING
if (!firebaseData) {
console.log("NOT getting data from FB");
return;
} else {
console.log("getting data from FB");
return;
}
};
Change up the code as follows:
const fetchDataFromDB = (user) => {
return db
.collection("users")
.doc(user)
.get()
.then((doc) => {
const data = doc.data();
console.log(data);
return data;
});
};
export const getSchedule = async (miles, user) => {
const firebaseData = await fetchDataFromDB(user);
console.log(firebaseData);
if (!firebaseData) {
console.log("NOT getting data from FB");
return;
} else {
console.log("getting data from FB");
return;
}
};
The point to remember about async await is that it doesn't really make asynchronous calls synchronous, it just makes them look that way so that your code is a bit less unwieldy with wrapped promises and the like. Every async function returns a promise, so if you want to deal with what it returns, you need to either deal with the promise directly (using .then...), or by using another await. In the latter case, you of course need to declare the consuming function as async as well.
With regards to the first function, there's no need for the async await there. Just return the promise. (Thanks #charlieftl for pointing out the problem in that code)

Categories

Resources