DEV Community

loading...
Cover image for Callback hell OR try catch hell (tower of terror)

Callback hell OR try catch hell (tower of terror)

Muhammad Ovi
JavaScript 🚀 (MERN) Developer 💻 YouTuber (where I barely make videos)
Originally published at muhammadovi.com ・3 min read

What are "Callbacks"?

A callback function is usually used as a parameter to another function.

The function that receives callback function is normally fetching data from a database, making an API request, downloading a file, which usually takes a while.

Assume getting some data from the API and the request takes around 2 seconds to complete.

Now, you can either wait for the API call to complete and then display your UI,

OR, you show everything else and show a loader where the API data needs to be shown.

In the API function, we pass some sort of "call back" function that replaces loader with actual data, so once the response is received from API

It calls the callback function with the data and, then our callback function replaces the loader.

Let's see this in action:

function getDataFromAPI(callbackFunction) {
  fetchSomeData().then((data) => {
    callbackFunction(data);
  });
}

getDataFromAPI(function replaceLoaderWithData(data) {
  // your awesome logic to replace loader with data
});
Enter fullscreen mode Exit fullscreen mode

OR

// from w3schools
function myDisplayer(sum) {
  document.getElementById('demo').innerHTML = sum;
}

function myCalculator(num1, num2, myCallback) {
  let sum = num1 + num2;
  myCallback(sum);
}

myCalculator(5, 5, myDisplayer);
Enter fullscreen mode Exit fullscreen mode

Okay, you already know this. We're not learning what callbacks are.

What is "callback hell"?

If your application logic is not too complex, a few callbacks seem harmless.
But once your project requirements start to increase, you will quickly find yourself piling layers of nested callbacks.

Like this:

getAreas(function (areas) {
  getTowns(function (towns) {
    getCities(function (cities) {
      getCountries(function (countries) {
        getContinents(function (continents) {
          getPlanets(function (planets) {
            getSolarSystems(function (solarSystems) {
              getGalaxies(function (galaxies) {
                // Welcome to the callback hell...
              });
            });
          });
        });
      });
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Of course, we can use JavaScript's Promise and move to .then & .catch.

getAreas().then(function (areas) {
  getTowns().then(function (towns) {
    getCities().then(function (cities) {
      getCountries().then(function (countries) {
        getContinents().then(function (continents) {
          getPlanets().then(function (planets) {
            getSolarSystems().then(function (solarSystems) {
              getGalaxies().then(function (galaxies) {
                // Welcome to the callback hell AGAIN...
              });
            });
          });
        });
      });
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Congrats! Welcome to Callback Hell.

Callback Hell, also known as Pyramid of Doom, is a slang term used to describe an unwieldy number of nested “if” statements or functions.

Async Await to the rescue!

Async await feels like heaven because it avoids the callback hell or pyramid of doom by writing asynchronous code in a clean line-by-line format.

The above code changes to this:

// assuming the environment supports direct async function
const areas = await getAreas();
const towns = await getTowns();
const cities = await getCities();
const countries = await getCountries();
const continents = await getContinents();
const planets = await getPlanets();
const solarSystems = await getSolarSystems();
const galaxies = await getGalaxies();

😳😲😳
// now this... looks awesome!!!
Enter fullscreen mode Exit fullscreen mode

BUT...

This is awesome until error handling comes into play because you end up with the try-catch tower of terror!

All your beautiful one-liners magically expand to at least five lines of code...

// assuming the environment supports direct async function

try {
  const areas = await getAreas();
} catch (err) {
  // handleError(err)
}

try {
  const towns = await getTowns();
} catch (err) {
  // handleError(err)
}

try {
  const cities = await getCities();
} catch (err) {
  // handleError(err)
}

try {
  const countries = await getCountries();
} catch (err) {
  // handleError(err)
}

// ... and so on.
Enter fullscreen mode Exit fullscreen mode

You can find yourself an easy way which is simply by appending the catch method to the end of each promise.

// assuming the environment supports direct async function
const areas = await getAreas().catch((err) => handleError(err));
const towns = await getTowns().catch((err) => handleError(err));
const cities = await getCities().catch((err) => handleError(err));
const countries = await getCountries().catch((err) => handleError(err));
const continents = await getContinents().catch((err) => handleError(err));
const planets = await getPlanets().catch((err) => handleError(err));
const solarSystems = await getSolarSystems().catch((err) => handleError(err));
const galaxies = await getGalaxies().catch((err) => handleError(err));
Enter fullscreen mode Exit fullscreen mode

This looks better, but! This is still getting repetitive.

Another better option is to create a standardized error handling function.

The function would first resolve the promise then returns an array.

In that array, the first element is the data and the second element is an error.

If there's an error then the data is null and the error is defined, like this:

async function promiseResolver(promise) {
  try {
    const data = await promise();
    return [data, null];
  } catch (err) {
    return [null, err];
  }
}
Enter fullscreen mode Exit fullscreen mode

Now when you call this function in your code you can destructure it to get a clean one-liner with error handling,
Or use a regular if statement if you want to do something else with the error.

Your main function would look something like this:

// assuming the environment supports direct async function
const [areas, areasErr] = await promiseResolver(getAreas);
const [towns, townsErr] = await promiseResolver(getTowns);
const [cities, citiesErr] = await promiseResolver(getCities);

if (citiesErr) {
  // do something
}

const [countries, countriesErr] = await promiseResolver(getCountries);
const [continents, continentsErr] = await promiseResolver(getContinents);
const [planets, planetsErr] = await promiseResolver(getPlanets);
const [solarSystems, solarSystemsErr] = await promiseResolver(getSolarSystems);
const [galaxies, galaxiesErr] = await promiseResolver(getGalaxies);

if (galaxiesErr) {
  // do something
}

// ... and so on.
Enter fullscreen mode Exit fullscreen mode

That's all folks! Hope you found this helpful, see you in the next one 😉

Discussion (20)

Collapse
lukeshiru profile image
LUKE知る

If you have nested promises, generally it means that you didn't structured your code as you should, you example of promises could look like this, which will run the promises in parallel:

Promise.all([
    getAreas(),
    getTowns(),
    getCities(),
    getCountries(),
    getContinents(),
    getPlanets(),
    getSolarSystems(),
    getGalaxies()
])
    .then(
        ([
            areas,
            towns,
            cities,
            countries,
            continents,
            planets,
            solarSystems,
            galaxies
        ]) => {
            // Do something here
        }
    )
    .catch(handleError);
Enter fullscreen mode Exit fullscreen mode

Good thing about Promise.all is that it's a promise itself, so is a nice way of putting several related promises together.

If you prefer to resolve even if there are errors, you can use Promise.allSetled instead. On the other hand, if you prefer to resolve one after the other, you could use a reduce with the array of promises, and every time one resolves you concat the output with the next one (same result of an array of resolved promises that you can use).

Either way, the thing is that even if async/await didn't existed, you shouldn't be nesting like that :D

Cheers!

Collapse
sargalias profile image
Spyros Argalias

I agree. Also, if your next function happens to depend on results from the previous function, you can chain them without nesting:

function getAreas() {}
function getTowns(areas) {}
function getCities(towns) {}
function getCountries(cities) {}

getAreas()
  .then(getTowns)
  .then(getCities)
  .catch(handleError);

// or inline
getAreas()
  .then(function getTowns(areas) {})
  .then(function getCities(towns) {})
  .then(function getCountries(cities) {})
  .catch(function handleError() {});
Enter fullscreen mode Exit fullscreen mode

Nice article overall though, keep it up :).

Collapse
ovi profile image
Muhammad Ovi Author

Yes, this would be a good approach too :)

Collapse
ovi profile image
Muhammad Ovi Author

But what if we have all the promise dependend on promise above them, and we want to handle error for each of them?

Collapse
lukeshiru profile image
LUKE知る

If every promise depends on the one above them, is even cleaner:

getAreas()
    .then(getTowns)
    .then(getCities)
    .then(getCountries)
    .then(getContinents)
    .then(getPlanets)
    .then(getSolarSystems)
    .then(getGalaxies)
    .catch(handleError);
Enter fullscreen mode Exit fullscreen mode

Or, as I mentioned previously, you can use a reduce:

[
    getAreas,
    getTowns,
    getCities,
    getCountries,
    getContinents,
    getPlanets,
    getSolarSystems,
    getGalaxies
].reduce(
    (previous, promise) => previous.then(promise),
    Promise.resolve()
);
Enter fullscreen mode Exit fullscreen mode

And you can even use an util that returns a tuple with [value, error] in that same reduce if you want to.

Still, the main point in my original comment is:

even if async/await didn't existed, you shouldn't be nesting like that.

What I mean by it is that, before async/await were a thing, nesting promises was seen as a bad practice, so the main argument for async/await shouldn't be "you don't have nesting", but actually "you have better ergonomics in some scenarios", or "is less confusing for people not used to JavaScript's asynchronicity".

Take it this way, you could also compare this two snippets, and in them async/await looks worst than using just promises:

const getAPI = async () => {
    try {
        const response = await fetch(API);
        console.log(await response.response.json());
    } catch (error) {
        console.error(error);
    }
};

// vs

const getAPI = () =>
    fetch(API)
        .then(response => response.json())
        .then(console.log)
        .catch(console.error);
Enter fullscreen mode Exit fullscreen mode

Cheers!

Collapse
basharath profile image
Basharath

You just need to pass the function(Promise) references in Promise.all

Collapse
lukeshiru profile image
LUKE知る

Not really. The functions return promises, but they are still functions and Promise.all expects an array of promises, not an array of Promise returning functions.

Thread Thread
basharath profile image
Basharath

Yes, but they are promises not functions returning promises.

Thread Thread
lukeshiru profile image
LUKE知る

Even if you write it like this:

const getAreas = () => Promise.resolve("areas");
Enter fullscreen mode Exit fullscreen mode

Or like this:

const getAreas = async () => await "areas";
Enter fullscreen mode Exit fullscreen mode

That's a function that returns a Promise. You can check it yourself:

const getAreas = () => Promise.resolve("areas");

getAreas instanceof Promise; // false
typeof getAreas === "function"; // true

// And if we actually run it...
getAreas() instanceof Promise; // true
typeof getAreas() === "function"; // false
Enter fullscreen mode Exit fullscreen mode

Same applies to fetch, so you can try the example above with fetch and the result will be the same.

Thread Thread
basharath profile image
Basharath
const promise = new Promise((res, rej) => {});

// promise is a promise.
Enter fullscreen mode Exit fullscreen mode
Thread Thread
lukeshiru profile image
LUKE知る

I know a promise is a promise. I'm starting to think you didn't understand the examples in the post and in my comment. You argue that Promise.all takes promises, which is true, but in the post and in my example, getAreas and the other functions like it return promises, but they aren't promises themselves. So you need to call them when passing them to Promise.all to get promises.

The snippet you just put in your comment assigns a new Promise to promise, but that effectively runs the Promise in place.

I gave you an example already of a web platform function that returns a promise. Take a look at fetch in MDN.

Collapse
alekseiberezkin profile image
Aleksei Berezkin

You can have one catch clause below all awaits

Collapse
ovi profile image
Muhammad Ovi Author

But what if we have all the promise dependend on promise above them, and we want to handle error for each of them?

Collapse
peerreynders profile image
peerreynders • Edited
async function tryAll(promises, name) {
  const resolved = [];
  const rejected = [];
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled
  const all = await Promise.allSettled(promises);
  for (const outcome of all)
    if (outcome.hasOwnProperty('value')) resolved.push(outcome.value);
    else rejected.push(outcome.reason);

  if (rejected.length < 1) return resolved;

  const error = new Error('tryAll encountered errors');
  error.name = name;
  error.rejected = rejected;
  throw error;
}

const ERROR_NAME_MULTI_REJECT = 'ErrorMultiReject';

function handleMultiReject(multiError) {
  for (const err of multiError.rejected)
    console.log('Multi Error', err.toString());
}

function routeError(err) {
  if (err.name === ERROR_NAME_MULTI_REJECT) handleMultiReject(err);
  else console.log('Other Error -', err.toString());
}

function makeError(source, name) {
  const error = new Error(`Error from ${source}`);
  error.name = name;
  return error;
}

const testFn = (result, source, name) =>
  result ? Promise.resolve(result) : Promise.reject(makeError(source, name));

async function main() {
  try {
    const one = await testFn('one', `testFnOne`, 'ErrorOne');
    const results = await tryAll(
      [
        testFn(null, `testFnTwo`, 'ErrorTwo'),
        testFn(null, `testFnThree`, 'ErrorThree'),
        testFn('four', `testFnFour`, 'ErrorFour'),
      ],
      ERROR_NAME_MULTI_REJECT
    );
    console.log('done');
  } catch (err) {
    routeError(err);
  }
}

main();
// "Multi Error", "ErrorTwo: Error from testFnTwo"
// "Multi Error", "ErrorThree: Error from testFnThree"
Enter fullscreen mode Exit fullscreen mode

Perhaps you meant something else - in which case chaining the error handlers may be worth considering:

// wrap the error with one that is uniquely named ...
async function wrapError(promise, name) {
  try {
    // https://jakearchibald.com/2017/await-vs-return-vs-return-await/#return-awaiting
    return await promise;
  } catch (err) {
    const error = new Error('Wrapped Error');
    error.name = name;
    error.wrapped = err;
    throw error;
  }
}

function makeChainedHandler(next, handler) {
  return next
    ? (err) => {
        handler(err);
        next(null);
      }
    : handler;
}

// ... so that the appropriate error handler
// to process the wrapped error can be retrieved
// which calls any other chained "aborted" handlers.  
//
function handleError(err) {
  if (err.hasOwnProperty('wrapped')) {
    const handler = handlers.get(err.name);
    if (handler) {
      handler(err.wrapped);
      return;
    }

    err = err.wrapped;
  }

  console.log('Other Error -', err.toString());
}

async function main() {
  try {
    const one = await wrapError(demoFnOne('one'), ERROR_NAME_ONE);
    const two = await wrapError(demoFnTwo(null), ERROR_NAME_TWO);
    const three = await wrapError(demoFnThree('three'), ERROR_NAME_THREE);
    const four = await wrapError(demoFnFour('four'), ERROR_NAME_FOUR);
    console.log('done');
  } catch (err) {
    handleError(err);
  }
}

// --- Begin Demo Support
const DEMO_ONE = 'DemoFnOne';
const DEMO_TWO = 'DemoFnTwo';
const DEMO_THREE = 'DemoFnThree';
const DEMO_FOUR = 'DemoFnFour';

const ERROR_NAME_ONE = 'ErrorOne';
const ERROR_NAME_TWO = 'ErrorTwo';
const ERROR_NAME_THREE = 'ErrorThree';
const ERROR_NAME_FOUR = 'ErrorFour';

const config = [
  [DEMO_ONE, ERROR_NAME_ONE],
  [DEMO_TWO, ERROR_NAME_TWO],
  [DEMO_THREE, ERROR_NAME_THREE],
  [DEMO_FOUR, ERROR_NAME_FOUR],
];

function makeError(fnName) {
  const error = new Error(`Error from ${fnName}`);
  error.name = 'Error' + fnName[0].toUpperCase() + fnName.slice(1);
  return error;
}

const makeDemoFn = (fnName) => (result) =>
  result ? Promise.resolve(result) : Promise.reject(makeError(fnName));

function makeErrorHandler(fnName) {
  return (err) => {
    if (err) {
      console.log(err.toString());
      return;
    }

    console.log(`Error: ${fnName} was aborted.`);
  };
}

function makeBoth([fnName, errName]) {
  const fn = makeDemoFn(fnName);
  const handler = makeErrorHandler(fnName);
  return [
    [fnName, fn],
    [errName, handler],
  ];
}

const [fns, handlers] = (() => {
  const fnEntries = [];
  const handlerEntries = [];
  let chained = null;

  // in reverse to set up the necessary abort chaining
  for (let i = config.length - 1; i >= 0; i -= 1) {
    const [fnEntry, [name, handler]] = makeBoth(config[i]);
    chained = makeChainedHandler(chained, handler);
    fnEntries.push(fnEntry);
    handlerEntries.push([name, chained]);
  }

  return [new Map(fnEntries), new Map(handlerEntries)];
})();

const demoFnOne = fns.get(DEMO_ONE);
const demoFnTwo = fns.get(DEMO_TWO);
const demoFnThree = fns.get(DEMO_THREE);
const demoFnFour = fns.get(DEMO_FOUR);

// --- End Demo Support

main();

// "ErrorDemoFnTwo: Error from DemoFnTwo"
// "Error: DemoFnThree was aborted."
// "Error: DemoFnFour was aborted."
Enter fullscreen mode Exit fullscreen mode
Collapse
peerreynders profile image
peerreynders

Your main function would look something like this

The thing is if you present code written in the manner described you have to expect to raise some eyebrows because you are ignoring error codes. Obviously you felt the need to do this - however work arounds like this are symptomatic of poor error design.

Defensive programming is messy

So messy in fact that the designers of Erlang invented "Let it Crash" (Programming Erlang 2e, p.88):

Many languages say you should use defensive programming and check the arguments to all functions. In Erlang, defensive programming is built-in. You should describe the behavior of functions only for valid input arguments; all other arguments will cause internal errors that are automatically detected. You should never return values when a function is called with invalid arguments. You should always raise an exception. This rule is called “Let it crash.”

However most runtimes don't have the luxury of letting the process die and having the supervisor deal with the crash (p.199):

If this process dies, we might be in deep trouble since no other process can help. For this reason, sequential languages have concentrated on the prevention of failure and an emphasis on defensive programming.

However there is another point to be made - not all errors are equal. Roughly speaking:

  • Expected errors. Errors will occur routinely during the operation of the software and therefore should be handled appropriately.
  • Exceptional errors. Errors that indicate that fundamental assumptions about the software and the environment that it is operating in have been violated. These type of errors cannot be handled at the local scope, so local processing is terminated and the error is passed upwards repeatedly until some kind of sensible compensating action can be taken.

Not all languages have exceptions but they have idioms for exceptional errors. Golang:

    // some processing
    result, err := doSomething()
    if err != nil {
        return err
    }

    // more processing ...
Enter fullscreen mode Exit fullscreen mode

Rust has the error propagation ? operator.

  let mut f = File::open("username.txt")?;
Enter fullscreen mode Exit fullscreen mode

i.e. for Ok(value) the value is is bound to the variable while an Error(error) is returned right then and there.

When a language like JavaScript supports exceptions the rule of thumb tends to be:

  • Use error values for expected errors.
  • Use exceptions for unexpected, exceptional errors.

So when we see

const [areas, areasErr] = await getAreas();
Enter fullscreen mode Exit fullscreen mode

areasErr is an expected error and should be handled, not ignored. And just because an error code is returned doesn't necessarily imply that getAreas() can't be a source of unexpected errors. When we see

const areas  = await getAreas();
Enter fullscreen mode Exit fullscreen mode

the code is implying that there aren't any expected errors to be handled locally but getAreas() can still be a source of unexpected errors.

With this in mind - 4.1.3. Rejections must be used for exceptional situations:

Bad uses of rejections include: When a value is asked for asynchronously and is not found.

i.e. a promise should resolve to an error value for expected errors rather than rejecting with the expected error. So when we see

const [areas, areasErr] = await promiseResolver(getAreas);
Enter fullscreen mode Exit fullscreen mode

there is a bit of a code smell because all errors deemed by getAreas() as exceptional are converted to expected errors at the call site and then are promptly ignored. There is an impedance mismatch between how getAreas() categorizes certain errors and how the code using it treats them. If you have no control over getAreas() then an explicit anti-corruption function (or an entire module for a "layer") may be called for to reorganize the error categorization (and the associated semantics), e.g. :

function myGetAreas() {
  try {
    return await getAreas();
  } catch (err) {
    if (ERROR_NO_ACTION_NAMES.includes(err.name)) return [];
    else throw err;
  }
}  
Enter fullscreen mode Exit fullscreen mode

so that the consuming code can be plainly

const areas = await myGetAreas();
Enter fullscreen mode Exit fullscreen mode

Compared to the above

const [areas, _areasErr] = await promiseResolver(getAreas);
Enter fullscreen mode Exit fullscreen mode

comes across as expedient (though noisy) and perhaps receiving less thought than it deserves.

Collapse
jfbrennan profile image
Jordan Brennan

try-catch tower of terror, love it! Haha! Glad to see someone else acknowledge async/await can be just as messy.

Collapse
jonsilver profile image
Jon Silver

There's an npm library that does exactly this. (Full disclosure: I wrote it and have been using it for a couple of years)
npmjs.com/package/@jfdi/attempt

Collapse
peerreynders profile image
peerreynders

All approaches have their trade-offs.

The Zen of Go:

Plan for failure, not success

Go programmers believe that robust programs are composed from pieces that handle the failure cases before they handle the happy path.

Go's errors are values philosophy is a recognized pain point.

Handling errors where they occur - midstream - obscures, in use case terminology, the happy path/basic flow/main flow/main success scenario. That doesn't mean that other flows/scenarios aren't important - on the contrary. That's why there are extensions/alternate flows/recovery flows/exception flows/option flows.

Clearly identifying the main flow in code is valuable.

Whether or not an IDE can collapse all the conditional error handling is beside the point - especially given that traditionally Java has been chastised for needing an IDE to compensate for all its warts.

to implement functional try/catch

Granted Go doesn't use a containing tuple like [error, result] but destructuring still directly exposes null or undefined values which is rather "un-functional" - typically the container (the tuple) is left as an opaque type while helper functions are used to work with the contained type indirectly (e.g. Rust's Result).

Now your criticism regarding try … catch is fair …

a variable ripe for mutation

… but JavaScript isn't a functional language (which typically is immutable by default and supports the persistent data structures to make that sustainable). However it is possible to take inspiration from Rust:

If a variable has unique access to a value, then it is safe to mutate it.

Rust: A unique perspective

e.g. for local variables with exclusive/unique access (i.e. aren't somehow shared via a closure) mutation can be acceptable.

So maintaining a context object to "namespace" all the values that need to stretch across the various try … catch scopes isn't too unreasonable:

const hasValue = (result) => result.hasOwnProperty('value');

class Result {
  static ok(value) {
    return new Result(value);
  }

  static err(error) {
    return new Result(undefined, error);
  }

  constructor(value, error) {
    if (typeof value !== 'undefined') this.value = value;
    else this.error = error;
  }

  get isOk() {
    return hasValue(this);
  }

  get isErr() {
    return !hasValue(this);
  }

  map(fn) {
    return hasValue(this) ? Result.ok(fn(this.value)) : Result.err(this.error);
  }

  mapOr(defaultValue, mapOk) {
    return hasValue(this) ? mapOk(this.value) : defaultValue;
  }

  mapOrElse(mapErr, mapOk) {
    return hasValue(this) ? mapOk(this.value) : mapErr(this.error);
  }

  andThen(fn) {
    return hasValue(this) ? fn(this.value) : Result.err(this.error);
  }

  // etc
}

const RESULT_ERROR_UNINIT = Result.err(new Error('Uninitialized Result'));

// To get around statement oriented
// nature of try … catch
// wrap it in a function
//
function doSomething(fail) {
  // While not strictly necessary
  // use `context` to "namespace"
  // all cross scope references
  // and initialize them to
  // sensible defaults.
  //
  const context = {
    success: false,
    message: '',
    result: RESULT_ERROR_UNINIT,
  };

  try {
    if (fail) throw new Error('Boom');
    context.success = true;
    context.result = Result.ok(context.success);
  } catch (err) {
    context.success = false;
    context.message = err.message;
    context.result = Result.err(err);
  } finally {
    console.log(context.success ? 'Yay!' : `Error: '${context.message}'`);
  }
  return context.result;
}

const isErrBoom = (error) => error instanceof Error && error.message === 'Boom';
const isErrNotTrue = (error) =>
  error instanceof Error && error.message === "Not 'true'";
const returnFalse = (_param) => false;
const isTrue = (value) => typeof value === 'boolean' && value;
const isFalse = (value) => typeof value === 'boolean' && !value;
const negate = (value) => !value;
const negateOnlyTrue = (value) =>
  isTrue(value) ? Result.ok(false) : Result.err(new Error("Not 'true'"));

// hide try … catch inside `doSomething()` to produce a `Result`
const result1 = doSomething(false); // 'Yay'
console.assert(result1.isOk, 'Should have been OK');
console.assert(result1.mapOr(false, isTrue), "Ok value isn't 'true'");
console.assert(
  result1.map(negate).mapOr(false, isFalse),
  "'map(negate)' didn't produce 'Ok(false)'"
);
console.assert(
  result1.andThen(negateOnlyTrue).mapOr(false, isFalse),
  "'andThen(negateOnlyTrue)' didn't produce 'Ok(false)'"
);
console.assert(
  result1
    .map(negate)
    .andThen(negateOnlyTrue)
    .mapOrElse(isErrNotTrue, returnFalse),
  "'andThen(negateOnlyTrue)' didn't produce 'Error(\"Not 'true'\")"
);

const result2 = doSomething(true); // "Error: 'Boom'"
console.assert(result2.isErr, 'Should have been Error');
console.assert(
  result2.mapOrElse(isErrBoom, returnFalse),
  "Message isn't 'Boom'"
);
console.assert(
  result2.map(negate).mapOrElse(isErrBoom, returnFalse),
  "'map(negate)' didn't preserve Error"
);
console.assert(
  result2.andThen(negateOnlyTrue).mapOrElse(isErrBoom, returnFalse),
  "'andThen(negateOnlyTrue)' didn't preserve Error"
);
Enter fullscreen mode Exit fullscreen mode
Collapse
ovi profile image
Muhammad Ovi Author

Thank you for sharing <3

Collapse
khorne07 profile image
Khorne07

Simple and powerful solution 👌