Node.js 承诺会间歇性地失败,即使在处理时也是如此

Posted

技术标签:

【中文标题】Node.js 承诺会间歇性地失败,即使在处理时也是如此【英文标题】:Node.js promise fails intermittently, even when handled 【发布时间】:2020-10-29 06:09:27 【问题描述】:

我正在通过创建一个简单的博客应用程序来学习使用 MongoDB。但是,我保存给定帖子的部分代码似乎偶尔会出现承诺问题,但并非总是如此,而代码是否成功似乎只是运气。

我数据库中的每个帖子都使用以下架构存储:


    title: String,
    author: String,
    body: String,
    slug: String,
    baseSlug: String,
    published:  type: Boolean, default: false 

slug 定义用于访问博客文章的链接,并根据博客文章的标题自动生成。但是,如果文章标题重复,slug 将在末尾添加一个数字以区别于类似文章,而baseSlug 将保持不变。例如:

我创建了帖子"My first post",并为其分配了"my-first-post"baseSlug。因为没有其他帖子具有相同的baseSlug,所以slug 也设置为"my-first-post"。 我创建了另一个名为"My first post" 的帖子,它被分配了"my-first-post"baseSlug。但是,由于另一个帖子具有相同的baseSlug,因此它被分配了slug "my-first-post-1"

为了创建这种行为,我在 Express 中编写了以下 addpost 路由:

app.post("/addpost", (req, res) => 
    let postInfo = req.body;

    for (key of Object.keys(postInfo)) 
        if (postInfo[key] == "true") postInfo[key] = true;
    

    let slug = postInfo.title
        .toLowerCase()
        .split(" ")
        .filter(hasNumber) // return /\d/.test(str);
        .slice(0, 5)
        .join("-");
    postInfo.slug = slug;

    var postData;

    Post.find( baseSlug: postInfo.slug , (error, documents) => 
        if (documents.length > 0) 
            let largestSlugSuffix = 0;

            for (let document of documents) 
                var fullSlug = document.slug.split("-");
                var suffix = fullSlug[fullSlug.length - 1];
                if (!isNaN(suffix)) 
                    if (parseInt(suffix) > largestSlugSuffix) 
                        largestSlugSuffix = suffix;
                    
                
            

            largestSlugSuffix++;
            postInfo.baseSlug = postInfo.slug;
            postInfo.slug += "-" + largestSlugSuffix;
         else 
            postInfo.baseSlug = postInfo.slug;
        

        postData = new Post(postInfo);
    )
        .then(() => 
            postData
                .save()
                .then(result => 
                    res.redirect("/");
                )
                .catch(err => 
                    console.log(err);
                    res.status(400).send("Unable to save data");
                );
        )
        .catch(err => 
            console.log(err);
            res.status(400).send("Unable to save data");
        );
);

这段代码似乎大部分时间都可以工作,但有时会失败,并输出以下内容:

TypeError: Cannot read property 'save' of undefined
    at C:\Users\User\BlogTest\app.js:94:18
    at processTicksAndRejections (internal/process/task_queues.js:94:5)

(供参考,我文件中的第 94 行是postData.save()

我怀疑这是因为函数主体的执行时间比它应该执行的要长,而且 postData 变量尚未定义。但是,由于.then() 回调函数,postData.save() 不应该在 Promise 完成之前执行。

为什么我的代码会这样?有什么办法可以解决吗?

【问题讨论】:

【参考方案1】:

问题是您将 Promise 与回调和闭包混合在一起。这不是它的预期工作方式。

当您链接 Promise 时,您在第一个 Promise 处理程序中返回的任何内容都将作为输入添加到下一个 Promise 处理程序。如果你返回一个 Promise,该 Promise 将在被发送到下一个 thenable 之前首先被解析。

所以你需要从你的 Promise 中返回 Promise,像这样:

app.post("/addpost", (req, res) => 
  let postInfo = req.body;

  for (key of Object.keys(postInfo)) 
    if (postInfo[key] == "true") postInfo[key] = true;
  

  let slug = postInfo.title
    .toLowerCase()
    .split(" ")
    .filter(hasNumber) // return /\d/.test(str);
    .slice(0, 5)
    .join("-");
  postInfo.slug = slug;

  // var postData; <-- Don't do that

  Post.find( baseSlug: postInfo.slug )
    .then((documents) => 
      if (documents.length > 0) 
        let largestSlugSuffix = 0;

        for (let document of documents) 
          var fullSlug = document.slug.split("-");
          var suffix = fullSlug[fullSlug.length - 1];
          if (!isNaN(suffix)) 
            if (parseInt(suffix) > largestSlugSuffix) 
              largestSlugSuffix = suffix;
            
          
        

        largestSlugSuffix++;
        postInfo.baseSlug = postInfo.slug;
        postInfo.slug += "-" + largestSlugSuffix;
       else 
        postInfo.baseSlug = postInfo.slug;
      
      return new Post(postInfo);
      // We could actually have called postData.save() in this method,
      // but I wanted to return it to exemplify what I'm talking about
    )
    // It is important to return the promise generated by postData.save().
    // This way it will be resolved first, before invoking the next .then method
    .then( (postData) =>  return postData.save(); )
    // This method will wait postData.save() to complete
    .then( () =>  res.redirect("/"); )
    .catch( (err) => 
      console.log(err);
      res.status(400).send("Unable to save data");
    );
);

使用 async/await 可以大大简化:

app.post("/addpost", async (req, res) => 
  try 
    let postInfo = req.body;

    for (key of Object.keys(postInfo)) 
      if (postInfo[key] == "true") postInfo[key] = true;
    

    let slug = postInfo.title
      .toLowerCase()
      .split(" ")
      .filter(hasNumber)
      .slice(0, 5)
      .join("-");
    postInfo.slug = slug;

    let documents = await Post.find( baseSlug: postInfo.slug );
    if (documents.length > 0) 
      let largestSlugSuffix = 0;

      for (let document of documents) 
        var fullSlug = document.slug.split("-");
        var suffix = fullSlug[fullSlug.length - 1];
        if (!isNaN(suffix)) 
          if (parseInt(suffix) > largestSlugSuffix) 
            largestSlugSuffix = suffix;
          
        
      
      largestSlugSuffix++;
      postInfo.baseSlug = postInfo.slug;
      postInfo.slug += "-" + largestSlugSuffix;
     else 
      postInfo.baseSlug = postInfo.slug;
    
    let postData = new Post(postInfo);
    await postData.save();
    res.redirect("/");
   catch (err) 
    console.log(err);
    res.status(400).send("Unable to save data");
  ;
);

【讨论】:

感谢您的帮助!我发现 async/await 解决方案既优雅又直观。接受的答案【参考方案2】:

你正在混合回调和承诺,虽然它可能会做一些事情,但我不确定它会做什么。您应该选择其中一种,不要尽可能多地混合它们。如果您使用的语言支持异步/等待,我建议选择 Promise,否则选择回调。

例如,您的外部处理程序可能是异步函数

app.post("/addpost", async (req, res) => 
  //...
)

您真正的错误是在处理Post.find 时,您在某种程度上使用回调和承诺来处理它,并且可能发生的事情是它的随机性,首先调用回调或承诺解决方案。既然你有一个异步函数,你就应该这样做,而不是两者都这样做:

try 
  const posts = await Post.find( baseSlug: postInfo.slug );

  // stuff you were doing in the callback
  const post = new Post(postInfo)
  
  // Now the promise code
  await post.save()

  // success!
  res.redirect("/");

 catch (err) 
  // With an async function you can just catch errors like normal
  console.log(err);
  res.status(400).send("Unable to save data");
  

如果你没有使用 webpack 或 typescript 并且不能以 es7 为目标,因此不能使用 async/await,那么我建议只使用回调,不要使用 .then.catch,这看起来更像:

function error(err) 
  console.log(err)
  res.status(400).send("Unable to save data")


Post.find( baseSlug: postInfo.slug , (err, documents) => 
  if (err) return error(err)

  // stuff you're doing in the callback now
  const post = new Post(postInfo)

  post.save((err) => 
    if (err) return error(err)
    
    // success!
    res.redirect("/");
  )
)

【讨论】:

以上是关于Node.js 承诺会间歇性地失败,即使在处理时也是如此的主要内容,如果未能解决你的问题,请参考以下文章

即使我只有一个参数,我也会收到“包装承诺不可迭代错误”

在 Node.js 承诺的环境中监控挂起的异步操作

即使某些异步作业失败,如何保持功能不失败?

如何在 Node.js expressjs 的异步对象方法中处理未处理的承诺拒绝?

使用youtube API和node.js添加youtube评论

在 node.js 中承诺一个递归函数