JavaScript async/await 異步函式

async/await 是 ES2017 (ES8) 引入的語法,讓你可以用「看起來像同步」的方式撰寫異步程式碼,大幅提升可讀性。它是建立在 Promise 之上的語法糖。

async 函式

在函式前面加上 async 關鍵字,這個函式就變成一個異步函式。async 函式永遠會回傳一個 Promise:

async function hello() {
    return 'Hello';
}

// async 函式回傳 Promise
hello().then(function(result) {
    console.log(result);  // Hello
});

// 等同於
function hello() {
    return Promise.resolve('Hello');
}

如果在 async 函式中 throw 錯誤,Promise 會變成 rejected 狀態:

async function fail() {
    throw new Error('出錯了');
}

fail().catch(function(err) {
    console.log(err.message);  // 出錯了
});

await 關鍵字

await 只能在 async 函式內部使用。它會「等待」一個 Promise 完成,並取得 Promise 的結果值:

function delay(ms) {
    return new Promise(function(resolve) {
        setTimeout(resolve, ms);
    });
}

async function demo() {
    console.log('開始');
    await delay(2000);  // 等待 2 秒
    console.log('2 秒後');
}

demo();

await 的好處是讓異步程式碼看起來像同步執行,不用再用 .then() 串接:

// 使用 Promise .then()
function fetchData() {
    return fetch('/api/user')
        .then(function(response) {
            return response.json();
        })
        .then(function(data) {
            console.log(data);
        });
}

// 使用 async/await - 更直觀
async function fetchData() {
    var response = await fetch('/api/user');
    var data = await response.json();
    console.log(data);
}

錯誤處理

使用 try/catch 來處理 async/await 的錯誤:

async function fetchUser() {
    try {
        var response = await fetch('/api/user');
        
        if (!response.ok) {
            throw new Error('請求失敗');
        }
        
        var data = await response.json();
        return data;
    } catch (err) {
        console.error('錯誤:' + err.message);
    }
}

你也可以在呼叫 async 函式時用 .catch() 處理錯誤:

async function riskyOperation() {
    throw new Error('Something went wrong');
}

riskyOperation().catch(function(err) {
    console.error(err.message);
});

多個異步操作

依序執行

如果操作之間有依賴關係,需要依序執行:

async function sequential() {
    var user = await fetchUser(1);        // 先取得使用者
    var posts = await fetchPosts(user.id); // 再取得該使用者的文章
    var comments = await fetchComments(posts[0].id); // 最後取得留言
    return comments;
}

並行執行

如果操作之間沒有依賴關係,可以用 Promise.all() 並行執行,提升效能:

async function parallel() {
    // 錯誤示範:依序執行,總共要等 3 秒
    var result1 = await delay(1000);
    var result2 = await delay(1000);
    var result3 = await delay(1000);
}

async function parallel() {
    // 正確做法:並行執行,只要等 1 秒
    var [result1, result2, result3] = await Promise.all([
        delay(1000),
        delay(1000),
        delay(1000)
    ]);
}

實際範例 - 同時取得多個 API 資料:

async function fetchAllData() {
    try {
        var [users, posts, comments] = await Promise.all([
            fetch('/api/users').then(r => r.json()),
            fetch('/api/posts').then(r => r.json()),
            fetch('/api/comments').then(r => r.json())
        ]);
        
        console.log(users, posts, comments);
    } catch (err) {
        console.error('其中一個請求失敗:' + err.message);
    }
}

在迴圈中使用 await

for 迴圈(依序執行)

async function processItems(items) {
    for (var i = 0; i < items.length; i++) {
        await processItem(items[i]);  // 依序處理每個項目
    }
}

forEach 的陷阱

forEach 不會等待 await,這是常見的錯誤:

// 錯誤:forEach 不會等待 await
async function wrong(items) {
    items.forEach(async function(item) {
        await processItem(item);  // 這些會同時開始執行!
    });
    console.log('完成');  // 會在處理完成前就執行
}

// 正確:使用 for...of
async function correct(items) {
    for (var item of items) {
        await processItem(item);
    }
    console.log('完成');  // 所有項目處理完才會執行
}

並行處理陣列

如果要並行處理陣列中的所有項目:

async function processAllParallel(items) {
    await Promise.all(items.map(function(item) {
        return processItem(item);
    }));
    console.log('全部完成');
}

實用範例

帶有重試機制的請求

async function fetchWithRetry(url, retries) {
    for (var i = 0; i < retries; i++) {
        try {
            var response = await fetch(url);
            return await response.json();
        } catch (err) {
            if (i === retries - 1) throw err;
            console.log('重試中... (' + (i + 1) + ')');
            await delay(1000);  // 等待 1 秒後重試
        }
    }
}

// 使用
var data = await fetchWithRetry('/api/data', 3);

設定逾時

function timeout(ms) {
    return new Promise(function(_, reject) {
        setTimeout(function() {
            reject(new Error('請求逾時'));
        }, ms);
    });
}

async function fetchWithTimeout(url, ms) {
    return Promise.race([
        fetch(url),
        timeout(ms)
    ]);
}

// 5 秒內沒回應就會拋出錯誤
var response = await fetchWithTimeout('/api/data', 5000);

注意事項

  1. await 只能在 async 函式內使用
// 錯誤
function test() {
    var data = await fetch('/api');  // SyntaxError
}

// 正確
async function test() {
    var data = await fetch('/api');
}
  1. Top-level await(ES2022)

在模組的最外層可以直接使用 await:

// 在 ES Module 中
var response = await fetch('/api/config');
var config = await response.json();

export default config;
  1. async/await 和 Promise 可以混用
async function getData() {
    return 'data';
}

// 可以用 .then()
getData().then(console.log);

// 也可以用 await
var data = await getData();