【问题标题】:Node.js promise fails intermittently, even when handledNode.js 承诺会间歇性地失败,即使在处理时也是如此
【发布时间】: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 完成之前执行。

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

【问题讨论】:

    标签: javascript node.js express mongoose 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("/");
      })
    })
    

    【讨论】:

      猜你喜欢
      • 2017-08-11
      • 2017-04-19
      • 2021-11-28
      • 2014-02-23
      • 1970-01-01
      • 2020-07-07
      • 1970-01-01
      • 2021-11-11
      • 1970-01-01
      相关资源
      最近更新 更多