1

Sometimes there is a need to call a function and handle the exception directly (with a try/catch), and other times there it is preferable to call the same function and receive a null or false if it would otherwise fail.

This idea or use-case is similar to Rails in ActiveRecord where you have a choice of calling User.find(1) or User.find!(1) — the former will return nil if not found and the latter will raise an exception.

Question: I am the author of these functions so I can design this in any way. Is there a prescribed pattern in javascript for this idea?


One idea is to just mimic what is done in ActiveRecord:

// returns API data or bubbles-up exception
function getDataFromAPI() {
  const response = callApi(); // function throws error if not 200
  return response.data;
}

// returns API data or null
function getDataFromAPI_() {
  try {
    return getDataFromAPI();
  } catch(e) {
    return null;
  }
}

Maybe another way is to create a generic wrapper for the try/catch, though this example doesn't account for arguments:

// returns API data, but will raise an exception if
// `response` has no `data` property...
function getDataFromAPI() {
  const response = callApi();
  return response.data;
}

// return function or null
function nullIfError(func) {
  try {
    return func();
  } catch(e) {
    return null;
  }
}

nullIfError(getDataFromAPI)

Or, add a parameter to conditially change it the behavior, but I don't like that it's always wrapped in a try/catch...

function getDataFromAPI(throwError = true) {
  try {
    const response = callApi();
    return response.data;
  } catch(e) {
    if (throwError) throw(e);
    else return null;
  }
}
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
burn
  • 31
  • 4
  • the error in your first example is down to poor handling of a possibly undefined value, not an error thrown from the API call itself so it would be more appropriate to accommodate that properly in the return ie. with optional chaining `return response?.data ?? null` – pilchard Dec 21 '22 at 15:09
  • Your second example can be adapted to work with arguments either by doing `nullIfError(() => getDataFromAPI(arg1, arg2))` or using `Function.prototype.bind` https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/Function/bind – Jim Nilsson Dec 21 '22 at 15:17
  • @pilchard I realize now that accessing a property on an object doesn't throw an exception. I've updated the example to better match my specific case, where calling the function will throw an error if the HTTP response is not 200. – burn Dec 21 '22 at 16:52
  • @JimNilsson Sorry, didn't mean to focus on the lack of arguments. I can get that to work, but more interested in the design pattern overal—is there a typical design pattern in JS that covers this idea? – burn Dec 21 '22 at 16:55
  • "*call the same function*" - no. You want to do different things, you call different functions. – Bergi May 04 '23 at 02:13
  • @burn ... Regarding the late provided sole answer, are there any questions left? – Peter Seliger May 15 '23 at 15:35

1 Answers1

0

Even though there is no real established pattern, one could introduce prototypal and/or non-prototypal implementations of possibly desirable function/method-modifiers (such as afterFinally, afterThrowing, after, before, around) which allow the interception, introspection and manipulation of a function's/method's control flow in terms of its arguments, return value and context at such a modified function's/method's call/apply/execution time.

As for the OP's use case the afterThrowing modifier comes in very handy.

The next provided example code shows both its implementation and usage.

function deliverData() {
  return { data: { foo: "bar "} };
}
function throwError() {
  throw new Error('API not available.');
}

function callMockedApi() {
  return [deliverData, throwError][Math.floor(Math.random() * 2)]();
}

function getDataFromAPI() {
  const response = callMockedApi();
  return response.data;
}
const getDataFromAPI_ = getDataFromAPI.afterThrowing(() => null);

console.log(getDataFromAPI_());
console.log(getDataFromAPI());
<script>
function isFunction(value) {
  return (
    'function' === typeof value &&
    'function' === typeof value.call &&
    'function' === typeof value.apply
  );
}
function getSanitizedTarget(value) {
  return value ?? null;
}

function afterThrowing(handler, target) {
  target = getSanitizedTarget(target);

  const proceed = this;
  return (
    isFunction(handler) &&
    isFunction(proceed) &&

    function afterThrowingType(...argumentArray) {
      const context = getSanitizedTarget(this) ?? target;

      let result;
      try {
        result = proceed.apply(context, argumentArray);
      } catch (exception) {
        result = handler.call(context, exception, argumentArray);
      }
      return result;
    }
  ) || proceed;
}

Reflect.defineProperty(Function.prototype, 'afterThrowing', {
  configurable: true,
  writable: true,
  value: afterThrowing,
});
</script>

More on this subject can be read up at ...

Peter Seliger
  • 11,747
  • 3
  • 28
  • 37