一、Express 异步错误捕获背景介绍

在 Express 开发中,异步操作是非常常见的,比如数据库查询、文件读取、网络请求等等。而异步操作可能会出现各种错误,像网络超时、数据库连接失败等。如果这些错误不能被正确捕获和处理,就会导致程序崩溃或者出现一些难以调试的问题,影响应用的稳定性。

1.1 简单异步操作示例

// 此处使用 Node.js+Express 技术栈
const express = require('express');
const app = express();

app.get('/async', (req, res) => {
    // 模拟一个异步操作
    setTimeout(() => {
        const error = new Error('模拟异步错误');
        // 这里抛出错误,但没有捕获
        throw error;
    }, 1000);
});

const port = 3000;
app.listen(port, () => {
    console.log(`服务器运行在端口 ${port}`);
});

在这个例子中,我们在一个路由里使用 setTimeout 模拟了一个异步操作,并且抛出了一个错误。由于没有对这个异步错误进行捕获处理,当这个错误发生时,Express 应用可能会崩溃。

二、传统异步错误捕获方式及问题

2.1 try...catch 无法捕获异步错误

在同步代码中,我们可以使用 try...catch 来捕获错误。

// Node.js+Express 技术栈
try {
    // 同步代码
    const result = JSON.parse('abc'); // 这里会抛出错误
} catch (error) {
    console.log('捕获到同步错误:', error.message);
}

但是对于异步代码,try...catch 就无能为力了。

// Node.js+Express 技术栈
try {
    setTimeout(() => {
        const error = new Error('异步错误');
        throw error;
    }, 1000);
} catch (error) {
    // 这里不会捕获到异步错误
    console.log('捕获到错误:', error.message);
}

在这个例子中,虽然有 try...catch 包裹,但是异步操作里抛出的错误并不会被捕获,因为当 setTimeout 里的代码执行时,try...catch 的作用域已经结束了。

2.2 Promise 的 then 和 catch

Promise 是处理异步操作的一种方式,我们可以使用 thencatch 来处理结果和错误。

// Node.js+Express 技术栈
function asyncOperation() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const error = false; // 模拟错误
            if (error) {
                reject(new Error('Promise 异步错误'));
            } else {
                resolve('操作成功');
            }
        }, 1000);
    });
}

asyncOperation()
   .then((result) => {
        console.log(result);
    })
   .catch((error) => {
        console.log('捕获到 Promise 错误:', error.message);
    });

在 Express 中使用 Promise 时,如果忘记添加 catch 处理,错误就会被漏过。

// Node.js+Express 技术栈
const express = require('express');
const app = express();

app.get('/promise', (req, res) => {
    asyncOperation().then(() => {
        res.send('成功');
    }); // 这里忘记添加 catch
});

const port = 3000;
app.listen(port, () => {
    console.log(`服务器运行在端口 ${port}`);
});

2.3 Async/Await 的 try...catch

async/await 是 ES2017 引入的异步编程方式,它让异步代码看起来更像同步代码,我们可以结合 try...catch 来捕获错误。

// Node.js+Express 技术栈
async function handleAsync() {
    try {
        const result = await asyncOperation();
        console.log(result);
    } catch (error) {
        console.log('捕获到 async/await 错误:', error.message);
    }
}

handleAsync();

但是在 Express 路由里直接使用 async/await 时,如果没有正确捕获错误,也会导致问题。

// Node.js+Express 技术栈
const express = require('express');
const app = express();

app.get('/async-await', async (req, res) => {
    const result = await asyncOperation();
    res.send(result);
    // 没有 try...catch 捕获错误
});

const port = 3000;
app.listen(port, () => {
    console.log(`服务器运行在端口 ${port}`);
});

三、终极方案实战

3.1 封装异步处理中间件

我们可以创建一个中间件来自动处理异步错误。

// Node.js+Express 技术栈
function asyncHandler(fn) {
    return (req, res, next) => {
        Promise.resolve(fn(req, res, next))
           .catch(next);
    };
}

const express = require('express');
const app = express();

function asyncOperation() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const error = true; // 模拟错误
            if (error) {
                reject(new Error('异步错误'));
            } else {
                resolve('操作成功');
            }
        }, 1000);
    });
}

app.get('/async-with-middleware', asyncHandler(async (req, res) => {
    const result = await asyncOperation();
    res.send(result);
}));

// 错误处理中间件
app.use((err, req, res, next) => {
    console.error(err);
    res.status(500).send('服务器内部错误');
});

const port = 3000;
app.listen(port, () => {
    console.log(`服务器运行在端口 ${port}`);
});

在这个例子中,asyncHandler 是一个高阶函数,它接收一个异步函数,并返回一个新的中间件函数。当异步函数抛出错误时,Promise.resolve 会将错误传递给 catch,然后通过 next 传递给错误处理中间件。

3.2 利用 Express 的全局错误处理

Express 提供了全局错误处理中间件,我们可以在应用中统一处理所有未捕获的错误。

// Node.js+Express 技术栈
const express = require('express');
const app = express();

// 模拟异步错误
app.get('/error', (req, res, next) => {
    setImmediate(() => {
        next(new Error('异步错误'));
    });
});

// 全局错误处理中间件
app.use((err, req, res, next) => {
    console.error('捕获到全局错误:', err.message);
    res.status(500).send('服务器内部错误');
});

const port = 3000;
app.listen(port, () => {
    console.log(`服务器运行在端口 ${port}`);
});

在这个例子中,当 /error 路由里的异步操作抛出错误时,通过 next 将错误传递给全局错误处理中间件,这样就可以统一处理错误,避免应用崩溃。

3.3 使用第三方库

有些第三方库可以帮助我们更方便地处理异步错误,比如 express-async-errors

// Node.js+Express 技术栈
const express = require('express');
const app = express();
require('express-async-errors');

function asyncOperation() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const error = true; // 模拟错误
            if (error) {
                reject(new Error('异步错误'));
            } else {
                resolve('操作成功');
            }
        }, 1000);
    });
}

app.get('/using-library', async (req, res) => {
    const result = await asyncOperation();
    res.send(result);
});

// 全局错误处理中间件
app.use((err, req, res, next) => {
    console.error('捕获到错误:', err.message);
    res.status(500).send('服务器内部错误');
});

const port = 3000;
app.listen(port, () => {
    console.log(`服务器运行在端口 ${port}`);
});

express-async-errors 库会自动捕获异步路由处理函数中的错误,并将其传递给错误处理中间件,这样我们就不需要手动封装异步处理中间件了。

四、应用场景

4.1 数据库操作

在进行数据库查询、插入、更新等操作时,这些操作通常是异步的。比如使用 mongoose 进行 MongoDB 操作。

// Node.js+Express+Mongoose 技术栈
const express = require('express');
const mongoose = require('mongoose');
const app = express();

const userSchema = new mongoose.Schema({
    name: String,
    age: Number
});

const User = mongoose.model('User', userSchema);

app.get('/find-user', async (req, res) => {
    try {
        const user = await User.findOne({ name: 'John' });
        res.send(user);
    } catch (error) {
        console.error('数据库查询错误:', error);
        res.status(500).send('数据库查询错误');
    }
});

mongoose.connect('mongodb://localhost:27017/mydb', {
    useNewUrlParser: true,
    useUnifiedTopology: true
});

const port = 3000;
app.listen(port, () => {
    console.log(`服务器运行在端口 ${port}`);
});

在这个例子中,使用 async/await 进行数据库查询,如果查询过程中出现错误,会被 try...catch 捕获并处理。

4.2 网络请求

当应用需要调用其他 API 时,网络请求也是异步操作。比如使用 axios 进行 HTTP 请求。

// Node.js+Express+Axios 技术栈
const express = require('express');
const axios = require('axios');
const app = express();

app.get('/fetch-data', async (req, res) => {
    try {
        const response = await axios.get('https://api.example.com/data');
        res.send(response.data);
    } catch (error) {
        console.error('网络请求错误:', error);
        res.status(500).send('网络请求错误');
    }
});

const port = 3000;
app.listen(port, () => {
    console.log(`服务器运行在端口 ${port}`);
});

这里使用 async/await 进行网络请求,如果请求失败,错误会被捕获并返回相应的错误信息。

五、技术优缺点

5.1 封装异步处理中间件的优缺点

优点

  • 可以统一处理异步错误,避免在每个路由处理函数中重复编写错误处理代码。
  • 代码结构清晰,易于维护。

缺点

  • 需要额外编写中间件代码,增加了一定的开发量。
  • 如果中间件编写不当,可能会影响应用的性能。

5.2 全局错误处理中间件的优缺点

优点

  • 可以捕获所有未处理的错误,保证应用不会因为未捕获的错误而崩溃。
  • 方便统一处理错误信息,返回给客户端统一的错误响应。

缺点

  • 可能会掩盖一些潜在的问题,因为所有错误都被统一处理,不容易定位具体的错误来源。
  • 对于一些特定类型的错误,可能无法进行精细的处理。

5.3 使用第三方库的优缺点

优点

  • 减少了开发量,不需要手动编写复杂的错误处理代码。
  • 库经过了大量的测试和优化,稳定性较高。

缺点

  • 增加了项目的依赖,可能会导致项目体积增大。
  • 如果库的更新不及时,可能会存在安全隐患。

六、注意事项

6.1 错误处理顺序

在 Express 中,错误处理中间件的顺序很重要。必须将错误处理中间件放在所有路由之后,否则可能无法正常捕获错误。

// Node.js+Express 技术栈
const express = require('express');
const app = express();

app.get('/error', (req, res, next) => {
    next(new Error('异步错误'));
});

// 错误处理中间件
app.use((err, req, res, next) => {
    console.error('捕获到错误:', err.message);
    res.status(500).send('服务器内部错误');
});

const port = 3000;
app.listen(port, () => {
    console.log(`服务器运行在端口 ${port}`);
});

如果将错误处理中间件放在路由之前,就无法捕获到路由中抛出的错误。

6.2 日志记录

在错误处理时,要做好日志记录。详细的日志可以帮助我们快速定位错误原因。可以使用 console.error 记录错误信息,也可以使用第三方日志库,如 winston

// Node.js+Express+Winston 技术栈
const express = require('express');
const app = express();
const winston = require('winston');

const logger = winston.createLogger({
    level: 'error',
    format: winston.format.json(),
    transports: [
        new winston.transports.Console(),
        new winston.transports.File({ filename: 'error.log' })
    ]
});

app.get('/error', (req, res, next) => {
    next(new Error('异步错误'));
});

app.use((err, req, res, next) => {
    logger.error(err.message);
    res.status(500).send('服务器内部错误');
});

const port = 3000;
app.listen(port, () => {
    console.log(`服务器运行在端口 ${port}`);
});

在这个例子中,使用 winston 记录错误信息,将错误信息同时输出到控制台和文件中。

6.3 避免无限循环

在错误处理中间件中,要避免出现无限循环的情况。如果在错误处理过程中再次抛出错误,并且没有正确处理,就会导致无限循环。

// Node.js+Express 技术栈
const express = require('express');
const app = express();

app.get('/error', (req, res, next) => {
    next(new Error('异步错误'));
});

app.use((err, req, res, next) => {
    // 这里避免无限循环
    if (err) {
        console.error(err.message);
        res.status(500).send('服务器内部错误');
    } else {
        next();
    }
});

const port = 3000;
app.listen(port, () => {
    console.log(`服务器运行在端口 ${port}`);
});

七、常见误区规避

7.1 忘记处理异步错误

在使用 Promise 或 async/await 时,一定要记得处理错误。比如在使用 Promise 时,要添加 catch 方法;在使用 async/await 时,要使用 try...catch 包裹。

7.2 错误处理中间件位置错误

如前面所述,错误处理中间件必须放在所有路由之后,否则无法正常工作。

7.3 错误处理不彻底

在错误处理中间件中,要确保对错误进行了彻底的处理,不能只是简单地忽略错误或者返回一个没有意义的错误信息。

八、文章总结

在 Express 开发中,异步错误捕获是一个非常重要的问题。传统的 try...catch 无法捕获异步错误,而 Promise 和 async/await 虽然提供了一定的错误处理方式,但如果使用不当,仍然会导致错误漏过。我们可以通过封装异步处理中间件、利用 Express 的全局错误处理和使用第三方库等终极方案来彻底解决异步错误问题。

在应用场景方面,主要涉及数据库操作和网络请求等异步操作。不同的解决方案有各自的优缺点,在实际开发中需要根据具体情况选择合适的方式。同时,要注意错误处理顺序、日志记录和避免无限循环等问题,并规避常见的误区,这样才能保证 Express 应用的稳定性和可靠性。