MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript在Node默认异步下的错误处理

2021-09-046.6k 阅读

JavaScript 在 Node 默认异步下的错误处理基础概念

异步编程与错误处理的紧密关系

在 Node.js 环境中,JavaScript 大量采用异步编程模型,这是因为 Node.js 旨在高效处理 I/O 密集型任务。异步操作,比如读取文件、网络请求等,不会阻塞主线程,允许程序在等待操作完成的同时继续执行其他任务。然而,这种异步特性给错误处理带来了独特的挑战。

传统的同步代码中,错误处理相对直观,通过try...catch块可以捕获代码执行过程中抛出的异常。例如:

function synchronousFunction() {
    throw new Error('同步错误');
}
try {
    synchronousFunction();
} catch (error) {
    console.error('捕获到同步错误:', error.message);
}

但在异步代码中,情况变得复杂。假设我们有一个简单的异步函数,使用setTimeout模拟异步操作:

function asyncFunction(callback) {
    setTimeout(() => {
        throw new Error('异步错误');
        callback();
    }, 1000);
}
try {
    asyncFunction(() => {
        console.log('异步操作完成');
    });
} catch (error) {
    console.error('捕获到异步错误:', error.message);
}

在上述代码中,try...catch块并不能捕获到异步函数内部抛出的错误。这是因为setTimeout中的回调函数是在主线程的事件循环的下一个 tick 中执行,此时try...catch块已经执行完毕。

Node.js 异步错误处理的常规模式

  1. 错误优先回调(Error - First Callback) 这是 Node.js 早期广泛使用的异步错误处理模式。在这种模式下,回调函数的第一个参数约定为错误对象,如果操作成功,该参数为null,否则为具体的错误实例。例如,读取文件的fs.readFile方法:
const fs = require('fs');
fs.readFile('nonexistentfile.txt', 'utf8', (err, data) => {
    if (err) {
        return console.error('读取文件错误:', err.message);
    }
    console.log('文件内容:', data);
});

在上述代码中,fs.readFile是一个异步操作,它接收文件名、编码格式以及一个回调函数。如果文件读取失败,err会包含错误信息,我们可以在回调函数中对其进行处理。

  1. Promise ES6 引入的 Promise 为异步操作提供了一种更优雅的方式,同时也改善了错误处理。Promise 有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。当 Promise 被resolve时,状态变为fulfilled;当被reject时,状态变为rejected。例如:
function readFilePromise(filePath, encoding) {
    return new Promise((resolve, reject) => {
        const fs = require('fs');
        fs.readFile(filePath, encoding, (err, data) => {
            if (err) {
                reject(err);
            } else {
                resolve(data);
            }
        });
    });
}
readFilePromise('nonexistentfile.txt', 'utf8')
  .then(data => {
        console.log('文件内容:', data);
    })
  .catch(err => {
        console.error('读取文件错误:', err.message);
    });

在上述代码中,readFilePromise返回一个 Promise 对象。如果文件读取成功,通过resolve传递数据;如果失败,通过reject传递错误。在then方法中处理成功的情况,在catch方法中处理错误。

  1. async/await async/await是基于 Promise 的语法糖,它让异步代码看起来更像同步代码,同时保持了异步的特性,并且错误处理也更加直观。例如:
const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);
async function readMyFile() {
    try {
        const data = await readFile('nonexistentfile.txt', 'utf8');
        console.log('文件内容:', data);
    } catch (err) {
        console.error('读取文件错误:', err.message);
    }
}
readMyFile();

在上述代码中,util.promisify将基于回调的fs.readFile方法转换为返回 Promise 的函数。在async函数中,通过await等待 Promise 解决。如果 Promise 被拒绝,await会抛出错误,我们可以使用try...catch块捕获并处理错误。

常见异步操作的错误处理

文件系统操作

  1. 读取文件 在 Node.js 中,使用fs.readFile读取文件是常见的异步操作。除了前面提到的错误优先回调和 Promise 方式,这里再深入探讨一些可能遇到的错误场景。例如,权限不足导致无法读取文件:
// 错误优先回调方式
const fs = require('fs');
fs.readFile('/root/importantfile.txt', 'utf8', (err, data) => {
    if (err && err.code === 'EACCES') {
        console.error('权限不足,无法读取文件');
    } else if (err) {
        console.error('其他读取文件错误:', err.message);
    } else {
        console.log('文件内容:', data);
    }
});
// Promise 方式
function readFilePromise(filePath, encoding) {
    return new Promise((resolve, reject) => {
        fs.readFile(filePath, encoding, (err, data) => {
            if (err && err.code === 'EACCES') {
                reject(new Error('权限不足,无法读取文件'));
            } else if (err) {
                reject(err);
            } else {
                resolve(data);
            }
        });
    });
}
readFilePromise('/root/importantfile.txt', 'utf8')
  .then(data => {
        console.log('文件内容:', data);
    })
  .catch(err => {
        if (err.message.includes('权限不足')) {
            console.error(err.message);
        } else {
            console.error('其他读取文件错误:', err.message);
        }
    });
// async/await 方式
async function readMyFile() {
    try {
        const data = await readFilePromise('/root/importantfile.txt', 'utf8');
        console.log('文件内容:', data);
    } catch (err) {
        if (err.message.includes('权限不足')) {
            console.error(err.message);
        } else {
            console.error('其他读取文件错误:', err.message);
        }
    }
}
readMyFile();

在上述代码中,通过检查错误对象的code属性,我们可以针对权限不足的特定错误进行处理。

  1. 写入文件 使用fs.writeFile写入文件也可能遇到各种错误。比如,目标路径不存在:
// 错误优先回调方式
const fs = require('fs');
fs.writeFile('/nonexistentdir/importantfile.txt', '这是文件内容', err => {
    if (err && err.code === 'ENOENT') {
        console.error('目标路径不存在');
    } else if (err) {
        console.error('其他写入文件错误:', err.message);
    } else {
        console.log('文件写入成功');
    }
});
// Promise 方式
function writeFilePromise(filePath, data) {
    return new Promise((resolve, reject) => {
        fs.writeFile(filePath, data, err => {
            if (err && err.code === 'ENOENT') {
                reject(new Error('目标路径不存在'));
            } else if (err) {
                reject(err);
            } else {
                resolve();
            }
        });
    });
}
writeFilePromise('/nonexistentdir/importantfile.txt', '这是文件内容')
  .then(() => {
        console.log('文件写入成功');
    })
  .catch(err => {
        if (err.message.includes('目标路径不存在')) {
            console.error(err.message);
        } else {
            console.error('其他写入文件错误:', err.message);
        }
    });
// async/await 方式
async function writeMyFile() {
    try {
        await writeFilePromise('/nonexistentdir/importantfile.txt', '这是文件内容');
        console.log('文件写入成功');
    } catch (err) {
        if (err.message.includes('目标路径不存在')) {
            console.error(err.message);
        } else {
            console.error('其他写入文件错误:', err.message);
        }
    }
}
writeMyFile();

这里通过检查err.codeENOENT来判断目标路径不存在的错误情况。

网络请求操作

  1. HTTP 请求 在 Node.js 中,使用http模块发起 HTTP 请求是常见的异步操作。例如,请求一个不存在的 URL:
const http = require('http');
const options = {
    hostname: 'nonexistentwebsite.com',
    port: 80,
    path: '/',
    method: 'GET'
};
const req = http.request(options, res => {
    let data = '';
    res.on('data', chunk => {
        data += chunk;
    });
    res.on('end', () => {
        console.log('响应数据:', data);
    });
});
req.on('error', err => {
    console.error('HTTP 请求错误:', err.message);
});
req.end();

在上述代码中,通过监听req对象的error事件来捕获 HTTP 请求过程中的错误,比如 DNS 解析失败、连接被拒绝等。

  1. TCP 连接 使用net模块创建 TCP 连接也可能遇到错误。例如,连接到一个未监听端口:
const net = require('net');
const client = new net.Socket();
client.connect(8080, '127.0.0.1', () => {
    console.log('已连接');
});
client.on('error', err => {
    console.log('TCP 连接错误:', err.message);
});

在上述代码中,通过监听client对象的error事件来处理 TCP 连接过程中的错误,如端口未监听、目标主机不可达等。

错误处理中的异常传播

函数调用链中的错误传递

在复杂的应用程序中,异步操作往往会形成函数调用链。例如,一个函数调用另一个异步函数,而这个异步函数又调用其他异步函数。在这种情况下,错误需要正确地在调用链中传播。

function asyncFunction1(callback) {
    setTimeout(() => {
        callback(new Error('asyncFunction1 错误'));
    }, 1000);
}
function asyncFunction2(callback) {
    asyncFunction1((err) => {
        if (err) {
            return callback(err);
        }
        setTimeout(() => {
            callback(null, 'asyncFunction2 成功');
        }, 1000);
    });
}
asyncFunction2((err, result) => {
    if (err) {
        console.error('最终捕获到错误:', err.message);
    } else {
        console.log('最终结果:', result);
    }
});

在上述代码中,asyncFunction1抛出的错误通过asyncFunction2传递给最终的回调函数,从而保证错误能够被正确处理。

全局错误处理

  1. 未捕获异常事件 Node.js 提供了process.on('uncaughtException')事件来捕获未被处理的异常。例如:
process.on('uncaughtException', (err) => {
    console.error('全局捕获到未处理的异常:', err.message);
    console.log('异常堆栈:', err.stack);
});
function throwUncaughtError() {
    throw new Error('未处理的异常');
}
throwUncaughtError();

在上述代码中,process.on('uncaughtException')可以捕获到throwUncaughtError函数中抛出的未被处理的异常。然而,使用uncaughtException应该谨慎,因为它可能会导致程序处于不可预测的状态,通常用于记录错误和进行紧急清理操作。

  1. 未处理的拒绝事件 对于未被处理的 Promise 拒绝,Node.js 提供了process.on('unhandledRejection')事件。例如:
process.on('unhandledRejection', (reason, promise) => {
    console.error('全局捕获到未处理的 Promise 拒绝:', reason.message);
    console.log('Promise 对象:', promise);
});
function createUnhandledPromise() {
    return new Promise((resolve, reject) => {
        reject(new Error('未处理的 Promise 拒绝'));
    });
}
createUnhandledPromise();

在上述代码中,process.on('unhandledRejection')可以捕获到未被catch处理的 Promise 拒绝情况,同样,这里主要用于记录错误信息,以便及时发现代码中的问题。

异步错误处理的最佳实践

明确错误类型和处理逻辑

在处理异步错误时,应该明确区分不同类型的错误,并针对每种类型制定合适的处理逻辑。例如,在网络请求中,对于 DNS 解析错误、连接超时错误等应该有不同的提示信息或处理策略。

const http = require('http');
const options = {
    hostname: 'nonexistentwebsite.com',
    port: 80,
    path: '/',
    method: 'GET',
    timeout: 2000
};
const req = http.request(options, res => {
    let data = '';
    res.on('data', chunk => {
        data += chunk;
    });
    res.on('end', () => {
        console.log('响应数据:', data);
    });
});
req.on('error', err => {
    if (err.code === 'ENOTFOUND') {
        console.error('DNS 解析错误,域名不存在');
    } else if (err.code === 'ECONNABORTED') {
        console.error('连接超时');
    } else {
        console.error('其他 HTTP 请求错误:', err.message);
    }
});
req.end();

在上述代码中,通过检查错误对象的code属性,对不同类型的网络请求错误进行了区分处理。

合理使用日志记录

在异步错误处理过程中,合理使用日志记录可以帮助快速定位和解决问题。可以使用console.logconsole.error等简单的日志输出,也可以使用专业的日志库,如winston

const winston = require('winston');
const logger = winston.createLogger({
    level: 'error',
    format: winston.format.json(),
    transports: [
        new winston.transport.Console()
    ]
});
function asyncFunctionWithError() {
    setTimeout(() => {
        try {
            throw new Error('异步操作错误');
        } catch (err) {
            logger.error({
                message: err.message,
                stack: err.stack
            });
        }
    }, 1000);
}
asyncFunctionWithError();

在上述代码中,使用winston库记录异步操作中抛出的错误,包括错误信息和堆栈跟踪,方便调试和排查问题。

避免过度使用全局错误处理

虽然process.on('uncaughtException')process.on('unhandledRejection')提供了全局捕获错误的能力,但过度依赖它们可能掩盖代码中的问题。应该尽量在局部对异步错误进行处理,确保错误在发生的地方就得到妥善处理,只有在无法在局部处理的情况下,才考虑使用全局错误处理。例如,在一个模块化的应用程序中,每个模块应该对自身的异步操作进行错误处理,而不是依赖全局错误处理机制。

// 模块 1
function module1Function(callback) {
    setTimeout(() => {
        try {
            throw new Error('模块 1 异步错误');
        } catch (err) {
            callback(err);
        }
    }, 1000);
}
// 模块 2
function module2Function() {
    module1Function((err) => {
        if (err) {
            console.error('模块 2 捕获到模块 1 的错误:', err.message);
        } else {
            console.log('模块 1 操作成功');
        }
    });
}
module2Function();

在上述代码中,module1Function捕获自身的错误并传递给module2Functionmodule2Function在局部对错误进行处理,而不是依赖全局错误处理。

通过以上对 JavaScript 在 Node 默认异步下错误处理的深入探讨,包括基础概念、常见异步操作的错误处理、异常传播以及最佳实践等方面,开发者可以更好地编写健壮、可靠的 Node.js 应用程序,有效应对异步编程中可能出现的各种错误情况。