Thenable

Thenable — любой объект с методом then(onFulfilled, onRejected). С ним работают await и Promise.resolve, что позволяет строить кастомные «промисоподобные» абстракции, не наследуясь от Promise.

Что это / Зачем

Promise — это лишь одна реализация контракта thenable. Если у объекта есть then(onFulfilled, onRejected?), его можно await-ить и передавать в Promise.resolve — он будет ассимилирован. Это нужно, чтобы:

  • сделать собственный лёгкий аналог Promise без лишних аллокаций;
  • интегрировать сторонние "промисоподобные" объекты (jQuery Deferred, библиотеки Bluebird, Q);
  • построить итерируемую асинхронную абстракцию, где состояние меняется многократно (Promise может разрешиться только один раз).

API / Синтаксис

// Минимальный thenable
const t = {
  then(onFulfilled) { onFulfilled(5); },
};
const x = await t; // 5

// Класс с отложенным значением
class Result {
  then(onFulfilled, onRejected) {
    this.onFulfilled = onFulfilled;
    this.onRejected = onRejected;
  }
  ready(data)  { this.onFulfilled && this.onFulfilled(data); }
  fail(error)  { this.onRejected  && this.onRejected(error); }
}

// thenable, который можно await-ить в цикле (повторно меняет состояние)
class Stream {
  constructor(arr) { this.state = arr; }
  then(onFulfilled, onRejected) {
    if (this.state.length) onFulfilled(this.state.shift());
    else onRejected(new Error('empty'));
    return this; // чейнинг без новых объектов
  }
}

Ключевые моменты

  • Promise сам построен на контракте thenable — это та же абстракция.
  • await thenable вызывает .then(onFulfilled, onRejected) и приостанавливает корутину до вызова одного из колбэков.
  • Thenable может разрешаться многократно — в отличие от Promise, состояние не фиксируется. Это позволяет await в цикле над одним объектом.
  • Возвращая this из then, экономим аллокации при чейнинге (за это платим иммутабельностью).
  • Для двунаправленного контракта используются onFulfilled и onRejected.

Подводные камни

  • Promise.resolve(thenable) "ассимилирует" thenable — если ваш .then сделан криво (вызывает колбэк синхронно или дважды), это сломает гарантии Promise.
  • Имена then и catch лучше не давать обычным методам бизнес-объектов — await obj неожиданно вызовет then и зависнет.
  • Полный контракт — .then(onFulfilled, onRejected). Без второго аргумента ошибки в thenable теряются.

🎓 Источники

  • 🎓 [Thenable и легковесный await в JavaScript] · 2019-05-07 · YouTube
    • Тезисы:
      • Thenable — объект с методом then, работает с await.
      • Каждый await дёргает then заново — состояние можно менять.
      • Promise построен на том же контракте, но фиксирует состояние после первого resolve.
      • Цепочка thenable через возврат this — односвязный список без лишних объектов.
    • Цитата:

      «Каждый раз, когда мы будем использовать await, это состояние мы можем менять. То есть тут существенное отличие от promise, который один раз свое состояние поменял, и все.»

    • Цитата:

      «Мы при помощи контракта thenable реализовали практически самый простой promise. То есть это реализация promise, написанная просто на контракте thenable.»

  • 🎓 [JavaScript Callable, Thenable, Promise, Array-like, Iterable, Observable, Streamable] · 2025-10-24 · YouTube
    • Тезис: thenable — один из шести контрактов "поведенческих интерфейсов" JS (наряду с callable, iterable, observable…).

См. также