Как обернуть функцию (метод) с помощью try-catch в современном javascript, используя декораторы (родные)?

как я могу сделать что-то вроде:

class LambdaFunctionExample {

  lambdaResponse = [1, 2 , 3]

  @tryCatchWrapper
  async handler(event) {
    const data = this.lambdaResponse;
    const result = await fetch(...)

    return {
      statusCode: 200,
      data,
      result
    };
  }

}

и перехватывать ошибки/события для целей POC.

Всегда ли приведенная выше реализация после транспилирования должна включать дополнительную функциональность, применяемую хуком-декоратором?

Peter Seliger 13.03.2024 18:43

Похожая проблема, решенная без декораторов, но с модификацией метода, была задана как «Как расширить/обернуть/перехватить/украсить класс (с помощью специальных функций, таких как трассировка)»

Peter Seliger 13.03.2024 18:53
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
1
2
118
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

для целей POC, как уже упоминалось

Я использую:

  • NodeJS v20
  • Машинопись 5.3
  • с помощью ESM
  • этап 3 TC39 Пропоральные декораторы
class LambdaFunctionExample {

  lambdaResponse = [1, 2, 3]

  @tryCatchWrapper
  async handler(event) {
    const data = this.lambdaResponse;
    const result = await fetch(...)

    return {
      statusCode: 200,
      data,
      result
    };
  }

}

export const handler =  (event) => new LambdaFunctionExample().handler(event)

функция декоратора:

export function tryCatchWrapper<T>(target: Function, { kind, name, ...context }) {
  if (kind === 'method')
    return async function (this: unknown, ...args: any) {
      try {
        console.info('name', name)
        return await target.call(this, ...args)
      } catch (error) {
        console.info('error catched')
        return {}
      }
    }
}

и бег

npm start # node index.js (as usual)

.tsconfig

  "compilerOptions": {
    // Module Setup
    "target": "ES2022", // as of today, I tried with ESNext and it did't work, so I need to use strictly ES2022
    //"module": "NodeNext", (also optional, but you imporant if you are using ESM)
    //"moduleResolution": "NodeNext", (also optional, but you imporant if you are using ESM)
    ...
  }

Спасибо за предоставление вышеуказанного решения. +1

Peter Seliger 13.03.2024 18:38

Поскольку вопрос ОП также касается функций декоратора как общего шаблона, а также из-за предложения/реализации декоратора JavaScript, специально ориентированного на функции класса, существует пробел, который может быть заполнен специализированными модификаторами методов, методами, которые реализованы в Function.prototype соответственно AsyncFunction.prototype.

Очевидными вариантами использования обертывания функций или методов, независимо от того, являются ли они синхронными или асинхронными, являются вокруг , до , after (покрытие после возврата), afterThrowing и afterFinally.

Вариант использования ОП прекрасно описан afterFinally.

А следующий примерный код берет пример класса OP и изменяет его в единую реализацию, которая служит испытательным стендом для настраиваемого/изменяемого поведения его единственной реализованной прототипной функции handler.

async function mockedFetch(isFetchFails) {
  return (isFetchFails === true)
    ? new Promise((_, reject) => setTimeout(reject, 3000, 'request timed out'))
    : new Promise(resolve => setTimeout(resolve, 1500, 'successfully fetched result'));
}

function afterFinallyHandler(result, exception = null, ...args) {

  // - the next following code block serves the demonstration purpose only.

  const reason = (typeof exception === 'string') && exception || null;
  const failure = (reason && { reason }) || (exception && { exception }) || {};

  console.info({
    args, value: result, ...failure
  });
  
  // - the next following single line of code covers the OP's
  //   actual implementation effort, since everything else got
  //   already abstracted away by the `afterFinally` modifier.

  return (exception === null) ? result : {};
}


class HandlerMethodExample {

  #isFetchFails = false;
  #lambdaResponse = [1, 2 , 3];

  constructor({ isFetchFails = false, isModifyHandler = false } = {}) {

    // implement modification depending on e.g. passed arguments.
    if (isModifyHandler) {

      this.handler = Object
        .getPrototypeOf(this)

        // the prototypal async `handler` method.
        .handler

        // modifying the prototypal async `handler` method.
        .afterFinally(afterFinallyHandler, this);
    }
    this.#isFetchFails = !!isFetchFails;
  }
  async handler(event) {
    const data = this.#lambdaResponse;

    const result = await mockedFetch(this.#isFetchFails);

    return { statusCode: 200, data, result };    
  }
}


(async () => {

  const failingAndUnmodifiedHandlerExample =
    new HandlerMethodExample({ isFetchFails: true });

  const notFailingAndUnmodifiedHandlerExample =
    new HandlerMethodExample();

  const failingButModifiedHandlerExample =
    new HandlerMethodExample({ isFetchFails: true, isModifyHandler: true });

  const notFailingButModifiedHandlerExample =
    new HandlerMethodExample({ isModifyHandler: true });

  let testResult;


  // console.info('\ntrigger async `failingAndUnmodifiedHandlerExample.handler` ...');

  // testResult = await failingAndUnmodifiedHandlerExample.handler('skipped', 'test');
  // console.info('... testResult ...', testResult);


  console.info('\ntrigger async `notFailingAndUnmodifiedHandlerExample.handler` ...');

  testResult = await notFailingAndUnmodifiedHandlerExample.handler('1st', 'test');
  console.info('... testResult ...', testResult);


  console.info('\ntrigger async `failingButModifiedHandlerExample.handler` ...');

  testResult = await failingButModifiedHandlerExample.handler('2nd', 'test');
  console.info('... testResult ...', testResult);


  console.info('\ntrigger async `notFailingButModifiedHandlerExample.handler` ...');

  testResult = await notFailingButModifiedHandlerExample.handler('3rd', 'test');
  console.info('... testResult ...', testResult);

})();
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
(function () {

  // BEGIN :: module scope
  'use strict';


  /* function type specifc detection helpers */


  /**
   * Detects whether a passed function type features a truly `writable`
   * `prototype` property.
   *
   * @param {Function} value
   *  Assumes a `'function'` type, but does not check for it.
   * @returns {boolean}
   *  Returns whether the passed value features a truly `writable`
   *  `prototype` property.
   */
  function hasWritablePrototype(value) {
    return Object.getOwnPropertyDescriptor(value, 'prototype')?.writable === true;
  }

  /**
   * Reaches for any value's built-in type-signature by making use of ...
   * 
   * ```
   * Object.prototype.toString.call(value);
   * ```
   *
   * ... which helps avoiding possibly
   * manipulated `toString` behavior.
   *
   * @param {any} value
   *  The to be processed value.
   * @returns {string}
   *  Returns the value's built-in type-signature
   *  like e.g. `'[object Date]'`.
   */
  function getBuiltInTypeSignature(value) {
    return Object.prototype.toString.call(value).trim();
  }

  /**
   * Reaches for a function's true stringified version by making use of ...
   * 
   * ```
   * Function.prototype.toString.call(value);
   * ```
   *
   * ... which helps avoiding possibly
   * manipulated `toString` behavior.
   *
   * @param {Function} value
   *  Assumes a `'function'` type, but does not check for it.
   * @returns {string}
   *  Returns a function's true/real stringified implementation.
   */
  function getFunctionSignature(value) {
    return Function.prototype.toString.call(value).trim();
  }

  /**
   * Reaches for a function's direct/immediate constructor-function.
   *
   * @param {Function} value
   *  Assumes a `'function'` type, but does not check for it.
   * @returns {string}
   *  Returns a function's direct/immediate constructor-function.
   */
  function getConstructorFunction(value) {
    return Reflect.getOwnPropertyDescriptor(

      Object.getPrototypeOf(value), 'constructor'
    )
    .value;
  }
  const AsyncFunction = getConstructorFunction(async function () {});


  /**
   * Detects any function type, which is ...
   *
   * ... the `typeof` operator not only returns the `'function'` string
   * for the processed `value`, but the latter also features both of a
   * function's call methods `call` and `apply`.
   *
   * @param {any} value
   *  The to be processed value.
   * @returns {boolean}
   *  A boolean value which indicates whether the tested value is a function.
   */
  function isFunction(value) {
    return (
      typeof value === 'function' &&
      typeof value.call === 'function' &&
      typeof value.apply === 'function'
    );
  }

  /**
   * Detects whether the passed `value` is any kind of arrow function,
   * either async or not.
   *
   * @param {any} value
   *  The to be processed value.
   * @returns {boolean}
   *  A boolean value which indicates whether the tested value is either
   *  kind of arrow function.
   */
  function isArrowFunctionType(value) {
    return (
      isFunction(value) &&
      /^(?:async\s*)?(?:\(.*?\)|[^(),=]+)\s*=>/.test(getFunctionSignature(value))
    );
  }

  /**
   * Detects whether the passed `value` is any kind of (non generator) async function,
   *  - either async arrow (expression)
   *  - or async function expression
   *  - or async function statement.
   *
   * @param {any} value
   *  The to be processed value.
   * @returns {boolean}
   *  A boolean value which indicates whether the tested value is an async function.
   */
  function isAsyncFunction(value) {
    return !!value && getBuiltInTypeSignature(value) === '[object AsyncFunction]';
  }

  /**
   * Detects whether the passed `value` is any kind of generator function,
   * either async or not.
   *
   * @param {any} value
   *  The to be processed value.
   * @returns {boolean}
   *  A boolean value which indicates whether the tested value is either
   *  kind of generator function.
   */
  function isGeneratorFunctionType(value) {
    const signature = !!value && getBuiltInTypeSignature(value);
    return signature && (
      signature === '[object GeneratorFunction]' ||
      signature === '[object AsyncGeneratorFunction]'
    );
  }

  /**
   * Detects whether the passed `value` is exclusively the
   * only known function type as far back as with/at ES3
   * (in addition to all the built-in constructor functions).
   *
   * @param {any} value
   *  The to be processed value.
   * @returns {boolean}
   *  A boolean value which indicates whether the tested value
   *  is exclusively the only known function type back at ES3.
   */
  function isES3Function(value) {
    return (
      isFunction(value) &&

      hasWritablePrototype(value) &&
      !isGeneratorFunctionType(value) &&

      // - detects any instance of a class that extends `Function`.
      !getFunctionSignature(getConstructorFunction(value)).startsWith('class ')
    );
  }


  /* modifier specifc reflection and configuration helpers */


  /**
   * Assumes the passed function type to be a modified function and
   * does augment it with modified function specific behavior/traits
   * which are ...
   *
   *  - a changed function `name` property
   *    with a modification specific prefix,
   *  - an additional `origin` property which
   *    refers to the original/unmodified function,
   *  - an additional `handler` property which refers
   *    to the modification specific handler function.
   *
   * @param {string} modifierName
   *  The specific modifier name which becomes the prefix part of the
   *  function's changed name.
   * @param {Function} origin
   *  The original/unmodified function's reference.
   * @param {Function} handler
   *  The reference of the modification specific handler function.
   * @param {Function} modified
   *  The modified `'function'` type (assumed, but not checked for).
   * @returns {Function}
   *  Returns the passed and augmented modified function.
   */
  function asConfiguredModification(modifierName, origin, handler, modified) {
    const nameDescriptor = Reflect.getOwnPropertyDescriptor(origin, 'name');

    Reflect.defineProperty(modified, 'name', {
      ...nameDescriptor,
      value: `modified::${ modifierName } ${ nameDescriptor.value }`,
    });
    Reflect.defineProperty(modified, 'origin', { value: origin });
    Reflect.defineProperty(modified, 'handler', { value: handler });

    // Reflect.defineProperty(modified, 'origin', { get: () => origin });
    // Reflect.defineProperty(modified, 'handler', { get: () => handler });

    return modified;
  }
  const modifierConfig = { writable: true, configurable: true };


  /* all modifier specifc implementations */


  /* function type validation guards for any modifier specific implementation */

  function runThrowingBaseValidationGuard(modifierName, proceed, handler, isAsync = false) {
    if (!isFunction(proceed)) {
      throw new TypeError([
        'The value delegated to',
        `\`${ isAsync && 'Async' || '' }Function.prototype.${ modifierName }\``,
        'needs to be at least a `Function` instance.',
      ].join(' '));
    }
    if (!isFunction(handler)) {
      throw new TypeError([
        'The', `${ isAsync && 'asynchronous ' || '' }\`${ modifierName }\``,
        'modifier\'s 1st `handler` parameter has to be at least a `Function` instance.',
      ].join(' '));
    }
  }
  function runThrowingAsyncValidationGuard(modifierName, proceed, handler) {
    runThrowingBaseValidationGuard(modifierName, proceed, handler, true);
    if (
      !isAsyncFunction(proceed) &&
      !isAsyncFunction(handler)
    ) {
      throw new TypeError([
        'In case the value delegated to', `\`AsyncFunction.prototype.${ modifierName }\``,
        'is a non asynchronous function type, the asynchronous', `\`${ modifierName }\``,
        'modifier\'s 1st `handler` parameter then has to be exclusively an `AsyncFunction`',
        'instance.',
      ].join(' '));
    }
    if (
      isAsyncFunction(proceed) &&

      !isAsyncFunction(handler) &&
      !isArrowFunctionType(handler) &&
      !isES3Function(handler)
    ) {
      throw new TypeError([
        'In case of modifying an asynchronous function type, the', `\`${ modifierName }\``,
        'modifier\'s 1st `handler` parameter has to be either an instance of `AsyncFunction`',
        'or an arrow function expression or an ES3 function type.',
      ].join(' '));
    }
  }


  /* +++ AFTER FINALLY +++ and +++ ASYNC AFTER FINALLY +++ */

  // - This modified function invokes the original function via a
  //   `try...catch` statement. Within the `finally` block the custom
  //   implemented after finally handler in addition to the provided
  //   arguments gets passed both values, the invocation `result` and
  //   the `error`. Moreover, the handle's return value becomes the
  //   modified function's return value. Thus, the modified function
  //   enables full control over how a function's after control flow
  //   is going to be handled.

  function afterFinallyModifier(handler, target) {
    'use strict';
    // see ... [https://github.com/tc39/proposal-function-implementation-hiding]
    'hide source';

    const proceed = this;
    runThrowingBaseValidationGuard('afterFinally', proceed, handler);

    target = target ?? null;
    return (

      isAsyncFunction(proceed) ||
      isAsyncFunction(handler)

      // - delegate the modification to the
      //   async `afterFinally` implementation.

    ) && AsyncFunction.prototype.afterFinally.call(proceed, handler, target) ||

    asConfiguredModification(
      'after::finally', proceed, handler,

      function /* afterFinallyType */(...args) {
        const context = this ?? target;

        let result;
        let error;
        try {

          result = proceed.apply(context, args);

        } catch (exception) {

          error = exception;

        } finally {

          result = handler.call(context, result, error, ...args);
        }
        return result;
      },
    );
  }
  function asyncAfterFinallyModifier(handler, target) {
    'use strict';
    // see ... [https://github.com/tc39/proposal-function-implementation-hiding]
    'hide source';

    const proceed = this;
    runThrowingAsyncValidationGuard('afterFinally', proceed, handler);

    const isAsyncProceed = isAsyncFunction(proceed);
    const isAsyncHandler = isAsyncFunction(handler);

    target = target ?? null;

    return asConfiguredModification(
      'after::returning', proceed, handler,

      // - The following implementations cover a combination
      //   of proceed- and handler-type specific optimizations.

      isAsyncProceed && (
        isAsyncHandler && (

          async function /* asyncAfterFinallyType */(...args) {
            const context = this ?? target;

            let result;
            let error;
            try {

              result = await proceed.apply(context, args);

            } catch (exception) {

              error = exception;

            } finally {

              result = await handler.call(context, result, error, ...args);
            }
            return result;
          }
        ) || (
          async function /* asyncAfterFinallyType */(...args) {
            const context = this ?? target;

            let result;
            let error;
            try {

              result = await proceed.apply(context, args);

            } catch (exception) {

              error = exception;

            } finally {

              result = handler.call(context, result, error, ...args);
            }
            return result;
          }
        )
      ) || (
        isAsyncHandler && (

          async function /* asyncAfterFinallyType */(...args) {
            const context = this ?? target;

            let result;
            let error;
            try {

              result = proceed.apply(context, args);

            } catch (exception) {

              error = exception;

            } finally {

              result = await handler.call(context, result, error, ...args);
            }
            return result;
          }
        ) || (
          async function /* asyncAfterFinallyType */(...args) {
            const context = this ?? target;

            let result;
            let error;
            try {

              result = proceed.apply(context, args);

            } catch (exception) {

              error = exception;

            } finally {

              result = handler.call(context, result, error, ...args);
            }
            return result;
          }
        )
      ),
    );
  }
  Reflect.defineProperty(Function.prototype, 'afterFinally', {
    ...modifierConfig, value: afterFinallyModifier,
  });
  Reflect.defineProperty(AsyncFunction.prototype, 'afterFinally', {
    ...modifierConfig, value: asyncAfterFinallyModifier,
  });


  // END :: module scope.

}());

// console.info(Function.prototype.afterFinally);
// console.info((async function () {}).constructor.prototype.afterFinally);
</script>

Предоставление причины, например технической или концептуальной ошибки, для отрицательного голосования по приведенному выше ответу действительно помогает аудитории как в рассуждениях о моем ответе, так и в улучшении самого ответа. Кроме того, голос -1 был получен через 5 минут после моего ответа. Я сомневаюсь, что избиратель сможет выработать хорошо профинансированное техническое/концептуальное суждение за такой короткий период времени.

Peter Seliger 13.07.2024 21:58

Другие вопросы по теме