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

Node.js 错误对象的结构与自定义错误类

2022-01-093.3k 阅读

Node.js 错误对象的结构

在 Node.js 开发中,错误处理是至关重要的一环。理解错误对象的结构,能帮助开发者更有效地调试和处理应用程序中出现的问题。

内置错误类型

Node.js 提供了一系列内置的错误类型,这些错误类型都是 Error 类的实例或其子类的实例。例如,SyntaxError 用于表示 JavaScript 语法错误,ReferenceError 用于表示引用不存在变量的错误等。

try {
    eval('var a ='); // 语法错误
} catch (error) {
    console.log(error instanceof SyntaxError); // true
    console.log(error.message); // 'Unexpected end of input'
}

上述代码中,我们通过 eval 函数故意引入一个语法错误,捕获后可以看到错误对象是 SyntaxError 类型,并且通过 message 属性获取到错误信息。

错误对象的属性

  1. message:该属性包含了关于错误的简短描述。这是开发者最常接触到的属性之一,它为调试提供了关键的线索。
try {
    throw new Error('这是一个自定义错误');
} catch (error) {
    console.log(error.message); // '这是一个自定义错误'
}
  1. name:表示错误的类型名称。对于内置错误,它对应相应的错误类型,如 SyntaxErrorTypeError 等;对于自定义错误,它默认是 Error,但可以在自定义错误类中修改。
try {
    throw new Error('自定义错误');
} catch (error) {
    console.log(error.name); // 'Error'
}
  1. stack:这是一个非常有用的属性,它包含了错误发生时的调用栈信息。通过调用栈,开发者可以追踪错误发生的具体位置和函数调用路径。
function a() {
    b();
}
function b() {
    throw new Error('错误在 b 函数中抛出');
}
try {
    a();
} catch (error) {
    console.log(error.stack);
    /*
    Error: 错误在 b 函数中抛出
        at b (/path/to/script.js:4:9)
        at a (/path/to/script.js:2:3)
        at /path/to/script.js:7:3
    */
}

自定义错误类

虽然 Node.js 的内置错误类型能满足很多常见的错误场景,但在实际开发中,特别是在大型项目里,开发者可能需要定义自己的错误类型,以便更好地组织和处理特定业务逻辑相关的错误。

基础自定义错误类

我们可以通过继承 Error 类来创建自定义错误类。

class MyCustomError extends Error {
    constructor(message) {
        super(message);
        this.name = 'MyCustomError';
    }
}
try {
    throw new MyCustomError('这是一个自定义错误实例');
} catch (error) {
    console.log(error.name); // 'MyCustomError'
    console.log(error.message); // '这是一个自定义错误实例'
    console.log(error instanceof MyCustomError); // true
    console.log(error instanceof Error); // true
}

在上述代码中,我们定义了 MyCustomError 类,它继承自 Error。在构造函数中,我们调用 super(message) 将错误信息传递给父类 Error,并且设置了 name 属性为 MyCustomError。这样,我们就可以通过 instanceof 操作符来判断捕获到的错误是否是 MyCustomError 类型。

自定义错误类添加额外属性

有时候,仅仅有错误信息是不够的,我们可能需要在错误对象中添加一些额外的属性,以便在处理错误时获取更多的上下文信息。

class DatabaseError extends Error {
    constructor(message, code) {
        super(message);
        this.name = 'DatabaseError';
        this.code = code;
    }
}
try {
    throw new DatabaseError('数据库连接失败', 1001);
} catch (error) {
    console.log(error.name); // 'DatabaseError'
    console.log(error.message); // '数据库连接失败'
    console.log(error.code); // 1001
}

这里我们定义了 DatabaseError 类,它除了继承 Error 类的属性外,还添加了一个 code 属性,用于表示数据库错误代码。这样在捕获到 DatabaseError 时,开发者可以根据 code 进一步处理不同类型的数据库错误。

自定义错误类的继承与多态

在复杂的项目中,可能会有多个层次的自定义错误类,通过继承来实现错误类型的细分和多态处理。

class NetworkError extends Error {
    constructor(message) {
        super(message);
        this.name = 'NetworkError';
    }
}
class HttpError extends NetworkError {
    constructor(message, statusCode) {
        super(message);
        this.name = 'HttpError';
        this.statusCode = statusCode;
    }
}
class NotFoundError extends HttpError {
    constructor(message) {
        super(message, 404);
        this.name = 'NotFoundError';
    }
}
try {
    throw new NotFoundError('资源未找到');
} catch (error) {
    console.log(error.name); // 'NotFoundError'
    console.log(error.message); // '资源未找到'
    console.log(error.statusCode); // 404
    console.log(error instanceof NotFoundError); // true
    console.log(error instanceof HttpError); // true
    console.log(error instanceof NetworkError); // true
    console.log(error instanceof Error); // true
}

在这段代码中,我们构建了一个错误类的继承体系。NetworkError 是基础的网络错误类,HttpError 继承自 NetworkError 并添加了 statusCode 属性,NotFoundError 又继承自 HttpError 并针对资源未找到的场景进行了特定的设置。通过这种继承关系,我们可以更灵活地处理不同层次的网络相关错误。

在 Node.js 模块中使用自定义错误类

当我们在 Node.js 项目中使用模块时,自定义错误类也需要正确地进行管理和使用。

导出自定义错误类

假设我们有一个名为 error.js 的文件,用于定义自定义错误类。

// error.js
class FileNotFoundError extends Error {
    constructor(filePath) {
        super(`文件 ${filePath} 未找到`);
        this.name = 'FileNotFoundError';
        this.filePath = filePath;
    }
}
module.exports = {
    FileNotFoundError
};

这里我们定义了 FileNotFoundError 类,并通过 module.exports 将其导出。

在其他模块中使用导出的自定义错误类

在另一个文件 main.js 中,我们可以引入并使用这个自定义错误类。

const { FileNotFoundError } = require('./error');
function readFileContent(filePath) {
    // 模拟文件不存在的情况
    if (!filePath.endsWith('.txt')) {
        throw new FileNotFoundError(filePath);
    }
    return '文件内容';
}
try {
    const content = readFileContent('test.js');
    console.log(content);
} catch (error) {
    if (error instanceof FileNotFoundError) {
        console.log(`处理文件未找到错误: ${error.message}`);
    } else {
        console.log(`其他错误: ${error.message}`);
    }
}

main.js 中,我们引入了 FileNotFoundError 类,并在 readFileContent 函数中根据条件抛出该错误。在捕获错误时,通过 instanceof 判断错误类型,并进行相应的处理。

与 Express.js 结合使用自定义错误类

Express.js 是 Node.js 中非常流行的 Web 应用框架。在 Express 应用中使用自定义错误类,可以更好地处理应用程序特定的错误。

创建 Express 应用并定义自定义错误类

const express = require('express');
const app = express();
class ValidationError extends Error {
    constructor(message) {
        super(message);
        this.name = 'ValidationError';
    }
}

这里我们创建了一个基本的 Express 应用,并定义了 ValidationError 类,用于处理数据验证相关的错误。

在 Express 路由中抛出自定义错误

app.post('/user', (req, res, next) => {
    const { name, age } = req.body;
    if (!name || typeof name!=='string') {
        throw new ValidationError('名称必须是字符串');
    }
    if (!age || typeof age!== 'number' || age < 0) {
        throw new ValidationError('年龄必须是正整数');
    }
    res.send('用户信息有效');
});

在这个 /user 路由中,我们对请求体中的 nameage 进行验证,如果验证失败,就抛出 ValidationError

全局错误处理中间件处理自定义错误

app.use((err, req, res, next) => {
    if (err instanceof ValidationError) {
        res.status(400).json({ error: err.message });
    } else {
        res.status(500).json({ error: '服务器内部错误' });
    }
});
const port = 3000;
app.listen(port, () => {
    console.log(`服务器在端口 ${port} 上运行`);
});

通过全局错误处理中间件,我们可以捕获在路由中抛出的错误。如果是 ValidationError,则返回状态码 400 并在响应体中包含错误信息;如果是其他错误,则返回状态码 500 并提示服务器内部错误。

在异步操作中处理自定义错误

在 Node.js 中,异步操作是非常常见的,如读取文件、数据库查询等。处理异步操作中的自定义错误需要一些特定的技巧。

使用回调函数处理异步自定义错误

function asyncTask(callback) {
    setTimeout(() => {
        try {
            throw new MyCustomError('异步任务出错');
        } catch (error) {
            callback(error);
        }
    }, 1000);
}
asyncTask((error) => {
    if (error) {
        console.log(`捕获到异步错误: ${error.message}`);
    } else {
        console.log('异步任务成功');
    }
});

在这个例子中,asyncTask 模拟了一个异步任务,通过 setTimeout 延迟 1 秒后执行。如果任务中抛出 MyCustomError,则通过回调函数将错误传递出去进行处理。

使用 Promise 处理异步自定义错误

function asyncPromiseTask() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            try {
                throw new MyCustomError('Promise 异步任务出错');
            } catch (error) {
                reject(error);
            }
        }, 1000);
    });
}
asyncPromiseTask()
  .then(() => {
        console.log('Promise 异步任务成功');
    })
  .catch((error) => {
        console.log(`捕获到 Promise 异步错误: ${error.message}`);
    });

当使用 Promise 进行异步操作时,如果在 Promise 的执行器函数中抛出错误,我们可以通过 reject 将错误传递出去,并在 catch 块中捕获处理。

使用 async/await 处理异步自定义错误

async function asyncAwaitTask() {
    await new Promise((resolve) => setTimeout(resolve, 1000));
    throw new MyCustomError('async/await 异步任务出错');
}
async function main() {
    try {
        await asyncAwaitTask();
        console.log('async/await 异步任务成功');
    } catch (error) {
        console.log(`捕获到 async/await 异步错误: ${error.message}`);
    }
}
main();

async/await 语法糖使得异步代码看起来更像同步代码。在 asyncAwaitTask 函数中抛出的自定义错误,可以在 main 函数的 try/catch 块中捕获并处理。

错误对象结构与自定义错误类的最佳实践

  1. 保持错误信息清晰:无论是内置错误还是自定义错误,错误信息应该尽可能清晰地描述问题。这有助于快速定位和解决问题。例如,在自定义错误类中,错误信息应该包含足够的上下文,如文件名、参数值等。
  2. 合理使用错误类型:根据错误的性质选择合适的内置错误类型或自定义错误类型。不要滥用自定义错误类,只有在需要特定业务逻辑相关的错误处理时才创建自定义错误。
  3. 错误处理的层次:在应用程序中,应该有不同层次的错误处理。在底层模块中,可以抛出详细的错误;在高层模块或全局错误处理中间件中,根据不同的错误类型进行统一的处理,如记录日志、返回合适的 HTTP 状态码等。
  4. 错误日志记录:对于重要的错误,特别是在生产环境中,应该记录详细的错误日志,包括错误对象的所有属性,如 messagenamestack 等。这有助于在出现问题时进行事后分析。

通过深入理解 Node.js 错误对象的结构以及灵活运用自定义错误类,开发者可以构建更加健壮、易于维护的应用程序。在处理错误时,要充分考虑各种场景,确保错误能够得到妥善的处理,从而提升应用程序的稳定性和可靠性。无论是小型项目还是大型企业级应用,良好的错误处理机制都是不可或缺的一部分。