Búsqueda de sitios web

typescript - ¿Cómo envolver una función (método) con try-catch en javascript moderno usando decoradores (nativos)?


  • sin Typescript experimentalDecorators: "true"
  • usando proposal-decorators de TC39

¿cómo puedo hacer algo como:

class LambdaFunctionExample {

  lambdaResponse = [1, 2 , 3]

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

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

}

y capturar errores/eventos con fines de prueba de concepto.

Respuesta #1

para fines de POC, como se mencionó

Estoy usando:

  • NodeJS v20
  • Typescript 5.3
  • usando ESM
  • propuesta de decoradores TC39 etapa 3
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)

función decoradora:

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

y corriendo

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 important if you are using ESM)
    //"moduleResolution": "NodeNext", (also optional, but important if you are using ESM)
    ...
  }

Respuesta #2

Dado que la pregunta del OP también toca las funciones decoradoras como un patrón genérico, y debido a que la propuesta/implementación de decoradores de JavaScript apunta específicamente a las características de las clases, hay un vacío que podría ser llenado por modificadores de método especializados, métodos que se implementan en Function.prototype y AsyncFunction.prototype, respectivamente.

Los casos de uso obvios para envolver funciones o métodos, independientemente de que sean síncronos o asíncronos, son around, before, after (cubriendo después de retornar), afterThrowing y afterFinally.

El caso de uso del OP está perfectamente cubierto por afterFinally.

Y el siguiente ejemplo de código proporciona toma el ejemplo de clase del OP y lo convierte en una única implementación que sirve como banco de pruebas para el comportamiento configurable/modificable de su única función handler implementada de manera prototípica.

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.log({
    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.log('\ntrigger async `failingAndUnmodifiedHandlerExample.handler` ...');

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


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

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


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

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


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

  testResult = await notFailingButModifiedHandlerExample.handler('3rd', 'test');
  console.log('... 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.log(Function.prototype.afterFinally);
// console.log((async function () {}).constructor.prototype.afterFinally);
</script>