Поймать обещание отказа в более позднее время

Как получить результат обещания позднее? В тесте я получаю письмо перед отправкой дальнейших запросов:

const email = await get_email();
assert.equal(email.subject, 'foobar');
await send_request1();
await send_request2();

Как я могу отправлять запросы во время медленного поиска электронной почты?

Сначала я подумала дождаться письма позже:

// This code is wrong - do not copy!
const email_promise = get_email();
await send_request1();
await send_request2();
const email = await email_promise;
assert.equal(email.subject, 'foobar');

Это работает, если get_email() успешен, но терпит неудачу, если get_email() завершается неудачно до соответствующего await, с полностью оправданным UnhandledPromiseRejectionWarning.

Конечно, я мог бы использовать Promise.all, вот так:

await Promise.all([
    async () => {
        const email = await get_email();
        assert.equal(email.subject, 'foobar');
    },
    async () => {
        await send_request1();
        await send_request2();
    },
]);

Тем не менее, это делает код намного труднее для чтения (это больше похоже на программирование на основе обратного вызова), особенно если последующие запросы фактически зависят от электронной почты, или происходит некоторое вложение. Можно ли сохранить результат/исключение для обещания и await его позже?

Если нужно, вот тестовый пример с ложными срабатываниями, которые иногда терпят неудачу, а иногда работают, со случайным временем Он никогда не должен выводить UnhandledPromiseRejectionWarning.

Ответы

Ответ 1

Вы можете использовать следующую вспомогательную функцию:

// Store the exception/result of a promise until a later time.
// Call like this:
// const buf = buffer_promise(LONG_RUNNING_PROMISE);
// // code to run in the meantime goes here
// const result = await buf();
function buffer_promise(promise) {
    let result, status;
    const chain = promise.then(res => {
        status = 'fulfilled';
        result = res;
    }).catch(err => {
        status = 'rejected';
        result = err;
    });
    return () => {
        return new Promise((resolve, reject) => {
            if (status) {
                ((status == 'fulfilled') ? resolve : reject)(result);
            } else {
                promise.then(resolve, reject);
            }
        });
    };
}

// End of helper function - example code follows    
// mock promises used below
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const send_request1 = () => wait(300), send_request2 = () => wait(200);
async function get_email() {
    await wait(Math.random() * 1000);
    if (Math.random() > 0.5) throw new Error('failure');
    return {subject: 'foobar'};
}

const assert = require('assert');
async function main() {
    const email_promise_buffer = buffer_promise(get_email());
    await send_request1();
    await send_request2();
    const email = await email_promise_buffer();
    assert.equal(email.subject, 'foobar');
};

(async () => {
    try {
        await main();
    } catch(e) {
        console.log('main error: ' + e.stack);
    }
})();

Ответ 2

const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const send_request1 = () => wait(300), send_request2 = () => wait(200);
async function get_email() {
    await wait(Math.random() * 1000);
    if (Math.random() > 0.5) throw new Error('failure');
    return {subject: 'foobar'};
}

const assert = require('assert');
async function main() {
    // catch possible error
    const email_promise = get_email().catch(e => e);
    await send_request1();
    await send_request2();
    // wait for result
    const email = await email_promise;
    // rethrow eventual error or do whatever you want with it
    if(email instanceof Error) {
      throw email;
    }
    assert.equal(email.subject, 'foobar');
};

(async () => {
    try {
        await main();
    } catch(e) {
        console.log('main error: ' + e.stack);
    }
})();

Ответ 3

В случае, если гарантируется, что отклонение обещания будет обработано позже, обещание может быть приковано к фиктивному catch для подавления обнаружения необработанного отказа:

try {
    const email_promise = get_email();
    email_promise.catch(() => {}); // a hack
    await send_request1();
    await send_request2();
    const email = await email_promise;
    assert.equal(email.subject, 'foobar');
} catch (err) {...}

Проблема с этим подходом состоит в том, что есть две параллельные подпрограммы, но код не выражает это, это обходной путь для того, что обычно делается с Promise.all. Единственная причина, по которой этот обходной путь выполним, состоит в том, что существует только 2 подпрограммы, и одна из них (get_email) должна быть get_email с then/await только один раз, поэтому ее часть (assert) может быть отложена. Проблема была бы более очевидной, если бы было 3 или более подпрограмм, или если подпрограммы включали несколько then/await.

В случае, если Promise.all вводит нежелательный уровень лямбда-вложения, этого можно избежать, написав подпрограммы как именованные функции, даже если они не используются где-либо еще:

async function assertEmail() {
    const email = await get_email();
    assert.equal(email.subject, 'foobar');
}

async function sendRequests() {
    await send_request1();
    await send_request2();
}

...

try {
    await Promise.all([assertEmail(), sendRequests()]);
} catch (err) {...}

Это приводит к чистому потоку управления и подробному, но более понятному и тестируемому коду.

Ответ 4

Итак, я хочу объяснить, почему мы ведем себя так в Node.js:

// Your "incorrect code" from before
const email_promise = get_email(); // we acquire the promise here
await send_request1(); // if this throws - we're left with a mess
await send_request2(); // if this throws - we're left with a mess
const email = await email_promise;
assert.equal(email.subject, 'foobar');

То есть причина, по которой мы ведем себя таким образом, заключается в том, что мы не имеем дело со сценарием "многократных отказов и, возможно, без очистки". Я не уверен, как вы закончили с длинным кодом для Promise.all но это:

await Promise.all([
    async () => {
        const email = await get_email();
        assert.equal(email.subject, 'foobar');
    },
    async () => {
        await send_request1();
        await send_request2();
    },
]);

На самом деле может быть так:

let [email, requestData] = await Promise.all([
  get_email(),
  send_request1().then(send_request2)
]);
// do whatever with email here

Это наверное то, что я бы сделал.

Ответ 5

Если вы настаиваете на использовании async/await, вы застряли на том, что имеете сейчас, так как ошибка будет проглочена без кэширования, как вы, или для отдельного переноса каждого вызова с помощью try/catch, что противоречит требованию сохранения вещей. DRY.

Поэтому я бы посоветовал просто заменить вашу буферную функцию следующей однострочной, так как в основном это просто переписанная форма .then(). Catch()

И сохраняйте все то же самое, поскольку я понял, что речь идет не о коде, а о стиле кодирования.

Чтобы снова вызвать ошибку, необходимо:

try { const email = await email_promise_buffer(); } catch( error ) {...}

который сразу же поймает, если буфер отклонен.

Таким образом, вы все еще должны кэшировать все промежуточные объекты обещания где-то.

// Store the exception/result of a promise until a later time.
// Call like this:
// const buf = buffer_promise(LONG_RUNNING_PROMISE);
// // code to run in the meantime goes here
// const result = await buf();
const buffer_promise = promise => () => promise.then( res => res ).catch( error => { throw error });
// End of helper function - example code follows    
// mock promises used below
const wait = async (ms) => await new Promise(resolve => setTimeout(resolve, ms));
const send_request1 = () => wait(300), send_request2 = () => wait(200);
async function get_email() {
    await wait(Math.random() * 1000);
    if (Math.random() > 0.5) throw new Error('failure');
    return {subject: 'foobar'};
}
async function main() {
    const email_promise_buffer = buffer_promise(get_email());
    await send_request1();
    await send_request2();
    const email = await email_promise_buffer();
    console.log(email.subject);
};

(async () => {
    try {
        await main();
    } catch(e) {
        console.log('main error: ' + e.stack);
    }
})();