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

JavaScript async和await的异常捕获

2023-06-236.8k 阅读

一、理解 async 和 await

在深入探讨异常捕获之前,我们先来回顾一下 asyncawait 的基本概念。async 函数是一种异步函数,它返回一个 Promise 对象。await 关键字只能在 async 函数内部使用,用于暂停 async 函数的执行,直到 Promise 被解决(resolved)或被拒绝(rejected)。

以下是一个简单的 async 函数示例:

async function getData() {
    return '这是异步获取的数据';
}
getData().then(result => console.log(result));

在上述代码中,getData 是一个 async 函数,它返回一个已解决的 Promise,并将字符串作为解决的值。通过 then 方法,我们可以处理这个解决的值。

await 的用法通常如下:

async function getData() {
    let promise = new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('数据获取成功');
        }, 1000);
    });
    let result = await promise;
    console.log(result);
}
getData();

在这个例子中,await 暂停了 getData 函数的执行,直到 promise 被解决,然后将解决的值赋给 result 变量。

二、异常捕获的重要性

在异步操作中,异常可能随时发生。例如,网络请求失败、数据库查询出错等。如果没有正确捕获和处理这些异常,它们可能会导致程序崩溃,影响用户体验。因此,掌握如何在 asyncawait 中捕获异常至关重要。

三、try - catch 块捕获异常

async 函数中,最常见的异常捕获方式是使用 try - catch 块。这种方式简洁明了,能够捕获 await 表达式抛出的异常。

以下是一个示例,模拟一个可能失败的网络请求:

async function makeRequest() {
    try {
        let response = await fetch('https://example.com/api/data');
        if (!response.ok) {
            throw new Error('网络请求失败');
        }
        let data = await response.json();
        console.log(data);
    } catch (error) {
        console.error('捕获到异常:', error.message);
    }
}
makeRequest();

在上述代码中,fetch 函数返回一个 Promise,如果网络请求失败,response.ok 会为 false,我们手动抛出一个错误。try - catch 块捕获这个错误,并在 catch 块中进行处理。

(一)多层嵌套的异常捕获

在实际应用中,async 函数可能包含多层嵌套的 await 操作。在这种情况下,try - catch 块同样适用。

async function complexOperation() {
    try {
        let step1 = await stepOne();
        let step2 = await stepTwo(step1);
        let step3 = await stepThree(step2);
        console.log('操作成功:', step3);
    } catch (error) {
        console.error('捕获到异常:', error.message);
    }
}

function stepOne() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('第一步完成');
        }, 1000);
    });
}

function stepTwo(result1) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (Math.random() > 0.5) {
                resolve(result1 + ',第二步完成');
            } else {
                reject(new Error('第二步失败'));
            }
        }, 1000);
    });
}

function stepThree(result2) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(result2 + ',第三步完成');
        }, 1000);
    });
}

complexOperation();

在这个例子中,complexOperation 函数按顺序执行 stepOnestepTwostepThreestepTwo 有一定概率失败,try - catch 块能够捕获任何一步中抛出的异常。

(二)错误类型的判断

catch 块中,我们可以根据错误类型进行不同的处理。

async function handleErrors() {
    try {
        let response = await fetch('https://example.com/api/data');
        if (!response.ok) {
            if (response.status === 404) {
                throw new Error('资源未找到');
            } else {
                throw new Error('其他网络错误');
            }
        }
        let data = await response.json();
        console.log(data);
    } catch (error) {
        if (error.message.includes('资源未找到')) {
            console.error('这是一个 404 错误:', error.message);
        } else {
            console.error('其他类型的错误:', error.message);
        }
    }
}
handleErrors();

通过对错误信息的判断,我们可以提供更具体的错误处理逻辑,提高用户体验。

四、不使用 try - catch 的异常捕获方式

除了 try - catch 块,还有其他方式可以捕获 asyncawait 中的异常。

(一)使用 Promise.catch

由于 async 函数返回一个 Promise,我们可以通过 catch 方法来捕获异常。

async function getDataWithoutTryCatch() {
    let response = await fetch('https://example.com/api/data');
    if (!response.ok) {
        throw new Error('网络请求失败');
    }
    let data = await response.json();
    return data;
}

getDataWithoutTryCatch()
   .then(data => console.log(data))
   .catch(error => console.error('捕获到异常:', error.message));

在这个例子中,getDataWithoutTryCatch 函数返回一个 Promise,我们通过 catch 方法捕获可能抛出的异常。这种方式与传统的 Promise 异常处理方式类似。

(二)自定义错误处理函数

我们可以创建一个自定义的错误处理函数,用于统一处理异常。

function handleError(error) {
    console.error('统一错误处理:', error.message);
}

async function makeRequestWithCustomHandler() {
    let response = await fetch('https://example.com/api/data');
    if (!response.ok) {
        throw new Error('网络请求失败');
    }
    let data = await response.json();
    return data;
}

makeRequestWithCustomHandler()
   .then(data => console.log(data))
   .catch(handleError);

通过这种方式,我们可以在多个 async 函数中复用错误处理逻辑,提高代码的可维护性。

五、全局异常捕获

在一些情况下,我们可能需要捕获整个应用程序中的未处理异常。在浏览器环境中,可以使用 window.onerror 事件来捕获全局异常。

window.onerror = function (message, source, lineno, colno, error) {
    console.log('全局捕获到异常:', message);
    console.log('来源:', source);
    console.log('行号:', lineno);
    console.log('列号:', colno);
    console.log('错误对象:', error);
    return true;
};

async function unhandledError() {
    let response = await fetch('https://nonexistent - url.com/api/data');
    let data = await response.json();
    console.log(data);
}

unhandledError();

在上述代码中,window.onerror 捕获了 unhandledError 函数中未处理的异常。注意,window.onerror 返回 true 可以阻止默认的错误处理行为。

在 Node.js 环境中,可以使用 process.on('uncaughtException') 来捕获未处理的异常。

process.on('uncaughtException', function (error) {
    console.log('全局捕获到异常:', error.message);
    console.log('堆栈跟踪:', error.stack);
});

async function unhandledNodeError() {
    throw new Error('Node.js 未处理异常');
}

unhandledNodeError();

这种全局异常捕获机制在调试和监控应用程序时非常有用,可以帮助我们及时发现并解决潜在的问题。

六、异常处理与代码结构

良好的异常处理机制有助于保持代码结构的清晰和可维护性。在编写 async 函数时,应尽量将异常处理逻辑与业务逻辑分离。

例如,我们可以将网络请求封装成一个单独的函数,并在该函数中处理异常。

async function fetchData() {
    try {
        let response = await fetch('https://example.com/api/data');
        if (!response.ok) {
            throw new Error('网络请求失败');
        }
        return await response.json();
    } catch (error) {
        console.error('网络请求异常:', error.message);
        return null;
    }
}

async function processData() {
    let data = await fetchData();
    if (data) {
        // 处理数据的逻辑
        console.log('处理数据:', data);
    }
}

processData();

在这个例子中,fetchData 函数负责处理网络请求和异常,processData 函数专注于数据处理逻辑。这样的代码结构更加清晰,易于理解和维护。

七、异常传递与中间件模式

在一些复杂的应用程序中,我们可能需要将异常传递到更高层次的代码进行处理。中间件模式可以很好地实现这一点。

以下是一个简单的中间件示例:

function middleware1(next) {
    return async function () {
        try {
            await next();
        } catch (error) {
            console.log('中间件 1 捕获到异常:', error.message);
            throw error;
        }
    };
}

function middleware2(next) {
    return async function () {
        try {
            await next();
        } catch (error) {
            console.log('中间件 2 捕获到异常:', error.message);
            throw error;
        }
    };
}

async function operation() {
    throw new Error('操作失败');
}

let chainedOperation = middleware1(middleware2(operation));
chainedOperation().catch(error => console.log('最终捕获到异常:', error.message));

在这个例子中,middleware1middleware2 依次捕获并传递异常。这种方式可以在不修改核心业务逻辑的情况下,添加通用的异常处理逻辑。

八、异常捕获与性能

虽然异常捕获对于程序的健壮性至关重要,但它也可能对性能产生一定的影响。频繁地抛出和捕获异常会导致额外的开销,因为 JavaScript 引擎需要创建错误对象并处理堆栈跟踪等操作。

为了减少性能影响,应尽量避免在性能关键的代码路径中使用异常来控制程序流程。例如,在循环中频繁抛出异常可能会显著降低程序的性能。

// 性能较差的示例
function badPerformance() {
    for (let i = 0; i < 1000000; i++) {
        try {
            if (i % 100 === 0) {
                throw new Error('模拟错误');
            }
        } catch (error) {
            // 处理错误
        }
    }
}

// 性能较好的示例
function goodPerformance() {
    for (let i = 0; i < 1000000; i++) {
        if (i % 100 === 0) {
            // 非异常方式处理
            continue;
        }
    }
}

在上述示例中,badPerformance 函数在循环中频繁抛出和捕获异常,而 goodPerformance 函数使用更高效的方式处理相同的逻辑。

九、与其他异步编程模式的结合

asyncawait 通常与其他异步编程模式结合使用,如 Promise.allPromise.race。在这些情况下,异常捕获也有相应的特点。

(一)Promise.all

Promise.all 接受一个 Promise 数组,并返回一个新的 Promise,只有当所有的 Promise 都被解决时,这个新的 Promise 才会被解决。如果其中任何一个 Promise 被拒绝,Promise.all 返回的 Promise 就会被拒绝。

async function multipleRequests() {
    try {
        let promises = [
            fetch('https://example.com/api/data1'),
            fetch('https://example.com/api/data2'),
            fetch('https://example.com/api/data3')
        ];
        let responses = await Promise.all(promises);
        let data = await Promise.all(responses.map(response => response.json()));
        console.log(data);
    } catch (error) {
        console.error('捕获到异常:', error.message);
    }
}

multipleRequests();

在这个例子中,只要任何一个 fetch 请求失败,Promise.all 就会抛出异常,try - catch 块会捕获这个异常。

(二)Promise.race

Promise.race 同样接受一个 Promise 数组,并返回一个新的 Promise,这个新的 Promise 会在数组中的任何一个 Promise 被解决或被拒绝时,就以相同的状态被解决或被拒绝。

async function raceRequests() {
    try {
        let promises = [
            new Promise((resolve, reject) => {
                setTimeout(() => {
                    resolve('第一个 Promise 解决');
                }, 2000);
            }),
            new Promise((resolve, reject) => {
                setTimeout(() => {
                    reject(new Error('第二个 Promise 拒绝'));
                }, 1000);
            })
        ];
        let result = await Promise.race(promises);
        console.log(result);
    } catch (error) {
        console.error('捕获到异常:', error.message);
    }
}

raceRequests();

在这个例子中,由于第二个 Promise 先被拒绝,Promise.race 返回的 Promise 也会被拒绝,try - catch 块捕获这个异常。

十、最佳实践总结

  1. 使用 try - catch 块:在 async 函数内部,尽量使用 try - catch 块来捕获异常,这样可以清晰地处理每个异步操作可能抛出的错误。
  2. 分离业务逻辑与异常处理:将异常处理逻辑与业务逻辑分离,使代码结构更清晰,易于维护。
  3. 全局异常捕获:在应用程序级别,使用 window.onerror(浏览器环境)或 process.on('uncaughtException')(Node.js 环境)来捕获未处理的异常,以便及时发现和解决问题。
  4. 注意性能:避免在性能关键的代码路径中频繁抛出和捕获异常,尽量使用非异常方式处理常见的错误情况。
  5. 结合其他异步模式:在使用 Promise.allPromise.race 等异步模式时,合理处理异常,确保整个异步流程的健壮性。

通过遵循这些最佳实践,我们可以在使用 asyncawait 进行异步编程时,有效地捕获和处理异常,提高应用程序的稳定性和可靠性。