چند سال پیش، در یک پروژه‌ی داشبورد، کدی به من رسید که پنج سطح callback تودرتو داشت. هر مرحله، یک درخواست API بود و هر درخواست، منتظر پاسخ قبلی. باگ عجیبی داشتیم: وقتی یکی از APIها با خطا برمی‌گشت، هیچ پیام مناسبی به کاربر نمی‌رسید و صفحه در حالت نصفه‌کاره می‌ماند. ساعتی وقت گذاشتم تا بفهمم کدام callback مقصر است؛ آن روز برایم روشن شد که Promise در جاوااسکریپت فقط یک سینتکس زیبا نیست؛ یک مدل ذهنی متفاوت برای مدیریت عملیات آسنکرون است که اگر درست درکش کنید، هم خوانایی کد چند برابر می‌شود و هم خطاها قابل‌ردیابی. در این مقاله، همان مسیری را می‌روم که امروز با تازه‌کارها طی می‌کنم: از callback hell و مفهوم Promise، تا زنجیره‌سازی، Promise.all، مدیریت خطا و async/await.

چرا Promise به وجود آمد؟

اگر تازه با جاوااسکریپت آشنا می‌شوید، اول آموزش جاوااسکریپت از صفر را بخوانید و بعد مفاهیم پایه جاوااسکریپت را مرور کنید. برای درک Promise، باید اول بفهمید چه دردی را درمان می‌کند. مشکل، callback hell بود — همان درخت تودرتوی توابع که هر لایه‌اش، یک مرحله‌ی آسنکرون را مدیریت می‌کرد:

// callback hell
getUser(userId, function (user) {
    getOrders(user.id, function (orders) {
        getOrderDetails(orders[0].id, function (details) {
            getShipping(details.shippingId, function (shipping) {
                console.log("Shipping info:", shipping);
            }, onError);
        }, onError);
    }, onError);
});

این کد سه مشکل اساسی دارد:

  • خوانایی پایین: منطق، به‌جای خطی پیش رفتن، به‌شکل هرمی تودرتو شده. پیدا کردن این‌که کدام callback کجاست، به سرعت سخت می‌شود.
  • مدیریت خطا تکراری: در هر مرحله، باید onError را جداگانه پاس بدهید. در نود درصد پروژه‌ها، توسعه‌دهنده یک جا فراموش می‌کند و خطاها بی‌صدا می‌مانند.
  • ترکیب سخت: اگر بخواهید دو عملیات را موازی اجرا کنید یا یکی از چند عملیات را اولین نتیجه بگیرید، callback تنها راه حل‌های پیچیده دارد.

Promise این سه مشکل را با یک مدل ذهنی متفاوت حل کرد: هر عملیات آسنکرون، یک شیء برمی‌گرداند که «قرار برای آینده» است. می‌توانید آن را به‌شکل خطی زنجیره کنید، خطاها را در یک نقطه متمرکز کنید و چند عملیات را با هم ترکیب کنید. تجربه‌ی من: بعد از تسلط بر Promise، حجم کد آسنکرون در پروژه‌های من به‌طور محسوس کاهش یافت و دیباگ آسان‌تر شد.

Promise، نه یک تکنیک جدید، بلکه یک قرارداد متفاوت است: به‌جای «این کار را انجام بده، بعد این را»، می‌گویید «وقتی تمام شد، این را انجام بده». همین تغییر زاویه، کد شما را خطی می‌کند.

سه حالت یک Promise

هر Promise، در هر لحظه دقیقاً در یکی از سه حالت زیر است:

حالتمعنیاتفاق بعدی
Pendingدر حال اجرامنتظر نتیجه
Fulfilledبا موفقیت تمام شد.then() اجرا می‌شود
Rejectedبا خطا تمام شد.catch() اجرا می‌شود

دو نکته‌ی مهم که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

  • تغییر حالت، یک‌طرفه است: Promise در ابتدا Pending است. وقتی به Fulfilled یا Rejected رسید، دیگر حالتش تغییر نمی‌کند. همین ویژگی، دلیل قابلیت اعتماد Promise است — یک پاسخ موفق نمی‌تواند بعداً به خطا تبدیل شود.
  • Fulfilled با Rejected یکی می‌شود: Promise یا موفق است یا ناموفق؛ حالت سومی وجود ندارد. همین سادگی، تصمیم‌گیری در کد را آسان می‌کند.

درک این سه حالت، پایه‌ی همه‌چیز است. اگر با رویدادها در جاوااسکریپت آشنا هستید، مدل Promise شبیه به Event Listener است — با این تفاوت که Promise فقط یک بار نتیجه می‌دهد و بعد از آن تمام می‌شود.

ساخت Promise و resolve/reject

ساخت Promise، با یک تابع سازنده انجام می‌شود که دو پارامتر دریافت می‌کند: resolve برای موفقیت و reject برای خطا:

const fetchUser = (userId) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (userId <= 0) {
                reject(new Error("Invalid user ID"));
                return;
            }

            resolve({ id: userId, name: "Ali" });
        }, 1000);
    });
};

سه نکته‌ی مهم در ساخت Promise که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

۱) همیشه Error برمی‌گردانید، نه رشته

// بد
reject("Something went wrong");

// خوب
reject(new Error("Something went wrong"));

Error شامل stack است که در دیباگ بسیار مفید است. اگر فقط رشته برگردانید، خطاها در لاگ، بی‌نام و بی‌ردپا می‌مانند.

۲) resolve/reject بعد از اولین فراخوانی بی‌اثر است

new Promise((resolve, reject) => {
    resolve("first");
    reject(new Error("second")); // این نادیده گرفته می‌شود
});

اولین فراخوانی برنده است. در پروژه‌های واقعی، این ویژگی گاهی باعث باگ‌های عجیب می‌شود — مثلاً وقتی یک شرط را اشتباه نوشتید و resolve زودتر صدا زده شده. همیشه با return بعد از reject، از ادامه‌ی اجرا جلوگیری کنید.

۳) خطای درون سازنده، Promise را Reject می‌کند

new Promise((resolve, reject) => {
    throw new Error("Unexpected error");
    // این خطای درون‌سازنده، به reject تبدیل می‌شود
});

این ویژگی به‌ظاهر جزئی است ولی در پروژه‌های واقعی، بسیار مفید — چون خطاهای برنامه‌نویسی، به‌طور خودکار به خطاهای Promise تبدیل می‌شوند و می‌توانید در یک نقطه مدیریتشان کنید.

then، catch و finally

سه متد اصلی برای کار با Promise:

then: نتیجه‌ی موفقیت

fetchUser(42)
    .then((user) => {
        console.log("User fetched:", user);
    });

catch: نتیجه‌ی خطا

fetchUser(-1)
    .then((user) => console.log(user))
    .catch((error) => {
        console.error("Failed to fetch user:", error.message);
    });

finally: اجرای نهایی، در هر حالت

showLoading();

fetchUser(42)
    .then((user) => displayUser(user))
    .catch((error) => displayError(error))
    .finally(() => hideLoading()); // در هر دو حالت اجرا می‌شود

الگوی finally، در پروژه‌های واقعی برای بستن لودرها بسیار پرکاربرد است — چون دقیقاً همان جایی است که می‌خواهید «کار تمام شد» را گزارش دهید، بدون توجه به این‌که موفق بود یا خیر. اگر با پایتون هم کار می‌کنید، همین مفهوم در finally بلوک try/except وجود دارد — که در مدیریت خطا در پایتون با جزئیات بررسی شده است.

زنجیره‌سازی: قدرت واقعی Promise

قدرت اصلی Promise در زنجیره‌سازی است. هر .then() خودش یک Promise برمی‌گرداند که می‌توانید در ادامه‌ی زنجیره از آن استفاده کنید:

fetchUser(42)
    .then((user) => fetchOrders(user.id))
    .then((orders) => fetchOrderDetails(orders[0].id))
    .then((details) => fetchShipping(details.shippingId))
    .then((shipping) => console.log("Shipping info:", shipping))
    .catch((error) => console.error("Something failed:", error));

مقایسه کنید با نسخه‌ی callback که در ابتدای مقاله آوردم. همان منطق، در یک ساختار خطی، بدون هرم تودرتو، با یک catch مرکزی برای همه‌ی خطاها. سه نکته‌ی مهم در زنجیره‌سازی که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

۱) بازگرداندن Promise از then، زنجیره را ادامه می‌دهد

اگر از داخل then یک Promise برگردانید، زنجیره منتظر آن می‌ماند:

fetchUser(42)
    .then((user) => {
        return fetchOrders(user.id); // زنجیره منتظر این Promise می‌ماند
    })
    .then((orders) => {
        console.log(orders);
    });

۲) بازگرداندن مقدار ساده، آن را Wrap می‌کند

Promise.resolve(5)
    .then((n) => n * 2)
    .then((n) => console.log(n)); // 10

هر مقدار معمولی که از then برگردانید، خودکار به یک Promise تبدیل می‌شود. این ویژگی، ترکیب کد همگام و آسنکرون را آسان می‌کند.

۳) خطای درون then، به catch بعدی می‌رود

fetchUser(42)
    .then((user) => {
        throw new Error("Processing failed");
    })
    .catch((error) => {
        console.error(error.message); // "Processing failed"
    });

این ویژگی، مدیریت خطا را بسیار متمرکز می‌کند — در یک زنجیره، فقط یک catch کافی است تا همه‌ی خطاها را بگیرد.

اگر با کد همگام کار می‌کنید و مفهوم زنجیره‌ی متدها برایتان جدید است، متدهای آرایه در جاوااسکریپت نمونه‌ی مشابهی از این الگو را نشان می‌دهد — زنجیره‌ی filter → map → reduce هم دقیقاً بر همین منطق استوار است.

Promise، زنجیره‌ای است که هر حلقه‌اش منتظر حلقه‌ی قبلی می‌ماند؛ این انتظار، کد شما را از تودرتویی نجات می‌دهد و خطاها را در یک نقطه جمع می‌کند.

مدیریت خطا در Promise

مدیریت خطا، نقطه‌ی قوت اصلی Promise است. سه الگوی رایج که در پروژه‌های واقعی به‌کار می‌برم:

۱) catch مرکزی در انتهای زنجیره

fetchUser(42)
    .then((user) => fetchOrders(user.id))
    .then((orders) => displayOrders(orders))
    .catch((error) => {
        console.error("Chain failed:", error);
        showErrorToUser("خطا در دریافت داده");
    });

۲) catch برای بازیابی

fetchUser(42)
    .catch((error) => {
        console.warn("Failed to fetch user, using guest mode");
        return { id: 0, name: "Guest" }; // مقدار جایگزین
    })
    .then((user) => displayUser(user));

این الگو برای «Fallback» بسیار مفید است — مثلاً اگر API کاربر جواب نداد، به‌جای نمایش خطا، یک کاربر مهمان نمایش دهید. همین رویکرد در سمت سرور هم کاربرد دارد — اصول کلی در امنیت API آمده است.

۳) catch دوباره پرتاب

fetchUser(42)
    .catch((error) => {
        logError(error); // لاگ کردن
        throw error; // پرتاب دوباره به لایه‌ی بالاتر
    })
    .catch((error) => {
        // این catch، خطای پرتاب‌شده را می‌گیرد
        showFinalError();
    });

نکته‌ی حیاتی که در پروژه‌های واقعی به آن رسیده‌ام: Promise بدون catch، یک بمب ساعتی است. اگر خطایی رخ دهد و هیچ catchی نباشد، خطا در کنسول ظاهر می‌شود ولی برنامه شما بی‌صدا به کارش ادامه می‌دهد — که در پروژه‌های بزرگ، به باگ‌های پنهان تبدیل می‌شود. یک قاعده‌ی سرانگشتی که رعایت می‌کنم: هر Promise، یک catch نهایی داشته باشد، حتی اگر فقط خطا را لاگ کند.

Promise.all: چند عملیات موازی

یکی از پرکاربردترین متدهای Promise، Promise.all است — وقتی چند عملیات مستقل دارید و می‌خواهید همه‌شان را موازی اجرا کنید:

const [user, orders, notifications] = await Promise.all([
    fetchUser(42),
    fetchOrders(42),
    fetchNotifications(42),
]);

در مقایسه با اجرای سری، این کد سه درخواست را همزمان می‌فرستد و زمان کل، به‌جای مجموع سه زمان، حداکثر آن‌ها است. در پروژه‌های واقعی، این تفاوت بین یک صفحه‌ی ۳ ثانیه‌ای و یک صفحه‌ی ۱ ثانیه‌ای است.

سه نکته‌ی مهم در Promise.all که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

  • یک خطا، کل را لغو می‌کند: اگر یکی از Promiseها Reject شود، Promise.all بلافاصله Reject می‌شود و بقیه‌ی نتایج نادیده گرفته می‌شوند. این رفتار برای سناریوی «همه باید موفق شوند» مناسب است.
  • ترتیب خروجی، همان ترتیب ورودی است: حتی اگر Promiseها به ترتیب دیگری تمام شوند، خروجی Promise.all همیشه به ترتیب آرایه‌ی ورودی است.
  • روی آرایه‌ی خالی: Promise.all([]) بلافاصله با یک آرایه‌ی خالی resolve می‌شود. این رفتار در پروژه‌های پویا مفید است.

الگوی پرکاربرد Promise.all در پروژه‌های واقعی، بارگذاری موازی داده‌های مستقل است. مثلاً در یک داشبورد که هم آمار کاربر، هم اعلان‌ها و هم وضعیت سفارش‌ها را نشان می‌دهد، هر سه درخواست را موازی می‌فرستم.

Promise.allSettled، race و any

علاوه بر Promise.all، سه متد دیگر وجود دارد که هرکدام سناریوی خاص خودش را دارد:

Promise.allSettled: منتظر همه، بدون توجه به خطا

const results = await Promise.allSettled([
    fetchUser(42),
    fetchOrders(42),
    fetchNotifications(42),
]);

results.forEach((result) => {
    if (result.status === "fulfilled") {
        console.log("Success:", result.value);
    } else {
        console.error("Failed:", result.reason);
    }
});

این متد منتظر همه می‌ماند و نتیجه‌ی هرکدام را جداگانه برمی‌گرداند. مناسب سناریوهایی که «اگر یکی شکست خورد، بقیه باید ادامه بدهند». مثلاً بارگذاری چند widget مستقل در یک داشبورد.

Promise.race: اولین پاسخ

const fastest = await Promise.race([
    fetchFromServerA(),
    fetchFromServerB(),
]);

اولین Promise که تمام شود (چه موفق چه ناموفق)، نتیجه‌ی نهایی است. کاربرد اصلی: مسابقه بین دو سرور برای کاهش زمان پاسخ، یا اعمال timeout:

const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error("Timeout")), 5000)
);

const result = await Promise.race([fetchData(), timeout]);

Promise.any: اولین موفقیت

const firstSuccess = await Promise.any([
    fetchFromServerA(),
    fetchFromServerB(),
    fetchFromServerC(),
]);

اولین Promise که با موفقیت تمام شود را برمی‌گرداند و خطاها را نادیده می‌گیرد — مگر همه شکست بخورند. مناسب سناریوی «چند منبع، اولین پاسخ موفق».

تفاوت این چهار متد را در یک جدول خلاصه می‌کنم:

متدچه‌وقت تمام می‌شودکاربرد
Promise.allوقتی همه موفق شوند، یا یکی شکست بخوردهمه باید موفق باشند
Promise.allSettledوقتی همه تمام شوند (موفق یا ناموفق)هرکدام مستقل
Promise.raceاولین پایان (موفق یا ناموفق)مسابقه یا timeout
Promise.anyاولین موفقیتچند منبع، اولین پاسخ موفق

Promisify: تبدیل callback به Promise

در پروژه‌های واقعی، گاهی با APIهای قدیمی روبرو می‌شوید که فقط callback می‌پذیرند. برای استفاده از آن‌ها در کد Promise-محور، باید آن‌ها را promisify کنید:

const promisify = (fn) => (...args) => {
    return new Promise((resolve, reject) => {
        fn(...args, (error, result) => {
            if (error) reject(error);
            else resolve(result);
        });
    });
};

// استفاده
const readFileAsync = promisify(fs.readFile);

readFileAsync("config.json", "utf8")
    .then((content) => JSON.parse(content))
    .then((config) => console.log(config))
    .catch((error) => console.error(error));

در Node.js، متد util.promisify همین کار را انجام می‌دهد. این الگو در پروژه‌های واقعی، به‌ویژه در کار با کتابخانه‌های قدیمی، ارزش زیادی دارد — چون به شما اجازه می‌دهد همه‌ی کد آسنکرون پروژه را با یک سبک یکدست بنویسید.

اصول مشابه این الگو در پایتون هم وجود دارد — تبدیل callback به Promise، شبیه به تبدیل کد callback-محور به async/await در پایتون است. اگر با پایتون کار می‌کنید، async/await در جاوااسکریپت همین مفهوم را با سینتکس متفاوتی توضیح می‌دهد.

async/await: خوانایی بالاتر

از ES2017، سینتکس async/await معرفی شد که Promise را با خوانایی کد همگام می‌نویسد:

// با Promise
function loadUserData(userId) {
    return fetchUser(userId)
        .then((user) => fetchOrders(user.id))
        .then((orders) => fetchOrderDetails(orders[0].id))
        .then((details) => ({ details }));
}

// با async/await
async function loadUserData(userId) {
    const user = await fetchUser(userId);
    const orders = await fetchOrders(user.id);
    const details = await fetchOrderDetails(orders[0].id);
    return { details };
}

در پروژه‌های واقعی، از زمانی که async/await را جدی گرفتم، خوانایی کد آسنکرون پروژه‌های من چند برابر شد. سه نکته‌ی مهم:

۱) async همیشه Promise برمی‌گرداند

async function getValue() {
    return 42;
}

// معادل:
function getValue() {
    return Promise.resolve(42);
}

حتی اگر از async یک مقدار ساده برگردانید، در بیرون Promise است. این یعنی می‌توانید async را با کد Promise-محور قدیمی ترکیب کنید.

۲) await فقط درون async کار می‌کند

// خطا
function getData() {
    const user = await fetchUser(42); // SyntaxError
}

// درست
async function getData() {
    const user = await fetchUser(42);
}

در ES2022، امکان top-level await در ماژول‌ها اضافه شد، ولی در پروژه‌های واقعی، این ویژگی به‌ندرت لازم می‌شود — چون اکثر عملیات، درون توابع async انجام می‌شوند.

۳) try/catch برای خطاها

async function loadData() {
    try {
        const user = await fetchUser(42);
        const orders = await fetchOrders(user.id);
        return { user, orders };
    } catch (error) {
        console.error("Failed to load data:", error);
        throw error;
    }
}

با async/await، مدیریت خطا با try/catch انجام می‌شود که برای توسعه‌دهندگانی که از زبان‌های دیگر آمده‌اند، طبیعی‌تر است. اصول کامل مدیریت خطا در مدیریت خطا در جاوااسکریپت آمده است.

async/await در حلقه — نکته‌ی ظریف

// اشتباه — همه موازی اجرا می‌شوند
async function processAll(items) {
    const promises = items.map(async (item) => await processItem(item));
    return Promise.all(promises);
}

// درست — اگر می‌خواهید سری باشند
async function processSequentially(items) {
    const results = [];
    for (const item of items) {
        results.push(await processItem(item));
    }
    return results;
}

در for...of، هر iteration منتظر await می‌ماند. ولی در map با arrow function async، اگر مراقب نباشید، همه‌ی عملیات‌ها موازی شروع می‌شوند. این تفاوت ظریف، در پروژه‌های واقعی منبع باگ‌های ظریفی است — خصوصاً در عملیاتی که روی سرور فشار می‌آورند.

async/await، Promise را در قالب کد همگام می‌ریزد؛ ولی قدرت واقعی‌اش در این است که بتوانید بین اجرای سری و موازی، آگاهانه انتخاب کنید.

الگوهای واقعی در پروژه‌ها

چند الگویی که با کمک Promise در پروژه‌های واقعی به الگوهای ذهنی من تبدیل شده‌اند:

الگوی اول: Retry با تأخیر نمایی

async function fetchWithRetry(url, retries = 3) {
    for (let attempt = 0; attempt < retries; attempt++) {
        try {
            return await fetch(url);
        } catch (error) {
            if (attempt === retries - 1) throw error;
            await new Promise((resolve) =>
                setTimeout(resolve, 1000 * Math.pow(2, attempt))
            );
        }
    }
}

این الگو در پروژه‌های پربازدید، تفاوت بین یک اپلیکیشن پایدار و یک اپلیکیشن لرزان را می‌سازد.

الگوی دوم: Timeout برای عملیات طولانی

function withTimeout(promise, ms) {
    return Promise.race([
        promise,
        new Promise((_, reject) =>
            setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
        ),
    ]);
}

const data = await withTimeout(fetchData(), 5000);

الگوی سوم: صف درخواست‌ها

const queue = [];
let active = 0;
const MAX_CONCURRENT = 3;

function enqueue(fn) {
    return new Promise((resolve, reject) => {
        queue.push({ fn, resolve, reject });
        processNext();
    });
}

function processNext() {
    if (active >= MAX_CONCURRENT || queue.length === 0) return;

    const { fn, resolve, reject } = queue.shift();
    active++;

    fn()
        .then(resolve)
        .catch(reject)
        .finally(() => {
            active--;
            processNext();
        });
}

این الگو در پروژه‌های اسکرپینگ یا پردازش دسته‌ای، از فشار آوردن به سرور جلوگیری می‌کند. اصول مشابه این کنترل همزمانی در وب اسکرپینگ با پایتون نیز وجود دارد.

تله‌هایی که در پروژه‌های واقعی دیده‌ام

در بازبینی کد پروژه‌های مختلف، این تله‌ها را زیاد دیده‌ام:

  • فراموش کردن catch: مهم‌ترین و پرتکرارترین. هر Promise باید یک catch نهایی داشته باشد. بدون آن، خطاها بی‌صدا می‌مانند و در روز بحران، هیچ‌جا لاگ نمی‌شوند.
  • عدم بازگرداندن Promise از then: اگر از then یک Promise برنگردانید، زنجیره منتظر آن نمی‌ماند و به‌طور موازی جلو می‌رود. نتیجه: باگ‌های ترتیبی عجیب.
  • استفاده از Promise در حلقه‌ی forEach: forEach منتظر await نمی‌ماند. اگر می‌خواهید سری باشند، از for...of استفاده کنید؛ اگر موازی، از Promise.all با map.
  • await روی مقدار غیر Promise: اگر روی یک مقدار معمولی await بزنید، بلافاصله برگردانده می‌شود و هیچ خطایی نمی‌دهد. ولی این اتفاق گاهی نشانه‌ی یک باگ منطقی است — چون انتظار داشتید یک Promise بگیرید.
  • خطای فقط-لاگ‌شده در catch: اگر در catch فقط console.error بزنید و خطا را دوباره پرتاب نکنید، لایه‌ی بالاتر از خطا بی‌خبر می‌ماند. اگر خطا واقعاً مهم است، دوباره پرتابش کنید.
  • مصرف حافظه در Promiseهای بی‌پایان: اگر Promiseها را در یک آرایه جمع می‌کنید و هرگز پاک نمی‌کنید، حافظه پر می‌شود. در پروژه‌های طولانی‌مدت، این باگ خاموش است.
  • نادیده‌گرفتن تفاوت Promise.all و allSettled: بعضی تیم‌ها فقط از all استفاده می‌کنند حتی در سناریوهایی که می‌خواهند بقیه ادامه بدهند. نتیجه: یک خطا، کل عملیات را متوقف می‌کند.
  • ترکیب نادرست async و callback: بعضی تیم‌ها یک API callback-محور را درون async می‌پیچند بدون این‌که واقعاً به Promise تبدیلش کنند. نتیجه: await بی‌اثر می‌شود.
  • خطاهای بی‌معنی در reject: reject("error") به‌جای reject(new Error("error")). بدون Error، stack trace وجود ندارد و دیباگ سخت می‌شود.
  • نادیده‌گرفتن ترتیب در Promise.all: خروجی Promise.all همیشه به ترتیب آرایه‌ی ورودی است، حتی اگر Promiseها با ترتیب متفاوتی تمام شوند. این رفتار در پروژه‌های واقعی گاهی باعث اشتباه در تشخیص می‌شود.
  • استفاده از Promise برای کد همگام: اگر عملیات شما همگام است (مثل محاسبه‌ی ساده)، wrap کردنش در Promise فقط پیچیدگی می‌افزاید بدون هیچ مزیتی.
  • عدم مستندسازی الگوهای پیچیده: زنجیره‌های طولانی Promise یا async‌های تودرتو، دو ماه بعد حتی توسط نویسنده‌اش هم سخت فهم می‌شوند. یک کامنت کوتاه، ارزش نگهداری زیادی دارد.

یک توصیه‌ی عملی از تجربه: در پروژه‌های واقعی، عادت کنید هر تابع async را با یک try/catch کامل بنویسید، مگر این‌که صریحاً بخواهید خطا به لایه‌ی بالاتر پرتاب شود. این انضباط کوچک، جلوی بسیاری از خطاهای بی‌صدا را می‌گیرد. اگر با ES6 و امکانات مدرن جاوااسکریپت آشنا نیستید، آموزش ES6 در جاوااسکریپت پیش‌نیاز خوبی است — چون Promise و async/await بخشی از همان تکامل زبان هستند. و اگر با fetch برای ارتباط با سرور کار می‌کنید، اصول آن در fetch API در جاوااسکریپت با مثال‌های عملی آمده است.

سخن آخر

Promise در جاوااسکریپت، از یک راه‌حل برای callback hell شروع شد ولی در پروژه‌های واقعی، به یک مدل ذهنی کامل برای مدیریت عملیات آسنکرون تبدیل شده است. سه نکته‌ی اصلی که در این مقاله به آن‌ها رسیدیم: اول، Promise یک قرارداد برای آینده است — سه حالت Pending، Fulfilled و Rejected، مدل ذهنی ساده ولی قدرتمندی می‌سازند؛ دوم، زنجیره‌سازی Promise، خوانایی و مدیریت خطا را چند برابر بهبود می‌دهد — به‌جای callback hell، یک زنجیره‌ی خطی با یک catch مرکزی؛ سوم، async/await خوانایی Promise را به سطح کد همگام می‌رساند — ولی باید تفاوت اجرای سری و موازی را بشناسید تا از آن درست استفاده کنید.

اگر امروز می‌خواهید در Promise ماهر شوید، سه کار کوچک پیشنهاد می‌کنم: یک callback hell ساده از پروژه‌ی قدیمی‌تان را با Promise بازنویسی کنید و تفاوت خوانایی را ببینید؛ سه درخواست API مستقل را با Promise.all موازی اجرا کنید و زمان کل را با حالت سری مقایسه کنید؛ و یک تابع async با try/catch بنویسید که در صورت خطا، مقدار جایگزین برگرداند — این الگو را در پروژه‌های واقعی بسیار استفاده می‌کنم. همین سه تمرین، ۹۰٪ مهارت‌های عملی Promise را در ذهن شما زنده می‌کند. مسیر طبیعی بعدی، fetch API در جاوااسکریپت، async/await در جاوااسکریپت و مدیریت خطا در جاوااسکریپت است. اگر تجربه‌ای از کار با Promise در پروژه‌های خودتان دارید — مخصوصاً اگر با یک باگ ظریف یا مشکل کارایی روبرو شده‌اید — در دیدگاه‌ها بنویسید؛ همین نکته‌های میدانی، برای خواننده‌ی بعدی از هر مستند رسمی ارزشمندتر است. ⏳