Elegant JavaScript error handling with the Either monad

Original author: James Sinclair
  • Transfer
Let's talk a little bit about how we handle errors. In JavaScript, we have a built-in language function for working with exceptions. We enclose the problematic code in the design try...catch. This allows you to register a normal execution path in the section try, and then deal with all exceptions in the section catch. Not a bad option. This allows you to focus on the current task without thinking about every possible mistake. Definitely better than clogging your code with endless ifs.

Without try...catchit is difficult to check the results of each function call for unexpected values. This is a useful design. But she has certain problems. And this is not the only way to handle errors. In this article, we will consider the use of the Either monad as an alternative try...catch.

Before continuing, I note a couple of points. The article assumes that you already know about function composition and currying. And a warning. If you have not come across monads before, they can seem really ... strange. Working with such tools requires a change in thinking. At first it can be hard.

Do not worry if you are immediately confused. Everyone has it. At the end of the article, I listed a few links that might help. Do not give up. These things get intoxicated as soon as they penetrate the brain.

Problem example


Before discussing exception issues, let's talk about why they even exist and why blocks appeared try...catch. To do this, let's look at a problem that I tried to make at least partially realistic. Imagine that we are writing a function to display a list of notifications. We have already managed (somehow) to return data from the server. But for some reason, the backend engineers decided to send it in CSV format, not JSON. Raw data might look something like this:

timestamp, content, viewed, href
2018-10-27T05: 33: 34 + 00: 00, @ madhatter invited you to tea, unread, https: //example.com/invite/tea/3801
2018-10-26T13: 47: 12 + 00: 00, @ queenofhearts mentioned you in 'Croquet Tournament' discussion, viewed, https: //example.com/discussions/croquet/1168
2018-10-25T03: 50: 08 + 00: 00, @ cheshirecat sent you a grin, unread, https: //example.com/interactions/grin/88

We want to display it in HTML. It might look something like this:


To simplify the task, just focus on processing each row of CSV data for now. Let's start with a few simple functions for processing strings. The first divides the text string into fields:

function splitFields(row) {
    return row.split('","');
}

The function is simplified here because it is educational material. We deal with error handling, not CSV analysis. If one of the messages contains a comma, all this will be terribly wrong. Please never use this code to analyze real CSV data. If you've ever had to analyze CSV data, use the well-tested CSV parsing library .

After splitting the data, we want to create an object. And so that each property name matches the CSV headers. Suppose we already somehow analyzed the title bar (more on that later). We have come to a point where something may go wrong. We got an error for processing. We throw an error if the length of the string does not match the title bar. ( _.zipObject Is a lodash function ).

function zipRow(headerFields, fieldData) {
    if (headerFields.length !== fieldData.length) {
        throw new Error("Row has an unexpected number of fields");
    }
    return _.zipObject(headerFields, fieldData);
}

After that, add a human-readable date to the object in order to display it in our template. It turned out a bit verbose, since JavaScript does not have perfect built-in support for date formatting. And again, we face potential problems. If an invalid date is encountered, our function throws an error.

function addDateStr(messageObj) {
    const errMsg = 'Unable to parse date stamp in message object';
    const months = [
        'January', 'February', 'March', 'April', 'May', 'June', 'July',
        'August', 'September', 'October', 'November', 'December'
    ];
    const d = new Date(messageObj.datestamp);
    if (isNaN(d)) {
        throw new Error(errMsg);
    }
    const datestr = `${d.getDate()} ${months[d.getMonth()]} ${d.getFullYear()}`;
    return {datestr, ...messageObj};
}

Finally, take the object and pass it through the template function to get the HTML string.

const rowToMessage = _.template(`
  • <%= content %>
  • `);

  • It would also be nice to print an error if it met:

    const showError = _.template(`
  • <%= message %>
  • `);

    When everything is in place, you can put together a function to process each line.

    function processRow(headerFieldNames, row) {
        try {
            fields = splitFields(row);
            rowObj = zipRow(headerFieldNames, fields);
            rowObjWithDate = addDateStr(rowObj);
            return rowToMessage(rowObj);
        } catch(e) {
            return showError(e);
        }
    }

    So the function is ready. Let's take a closer look at how it handles exceptions.

    Exceptions: the good part


    So what's good at try...catch? It should be noted that in the above example, any of the steps in the block trymay cause an error. In zipRow()and addDateStr()we intentionally throw out errors. And if a problem arises, just catch the error and display any message on the page. Without this mechanism, the code becomes really ugly. Here's how it might look. Assume that the functions do not throw errors, but return null.

    function processRowWithoutExceptions(headerFieldNames, row) {
        fields = splitFields(row);
        rowObj = zipRow(headerFieldNames, fields);
        if (rowObj === null) {
            return showError(new Error('Encountered a row with an unexpected number of items'));
        }
        rowObjWithDate = addDateStr(rowObj);
        if (rowObjWithDate === null) {
            return showError(new Error('Unable to parse date in row object'));
        }
        return rowToMessage(rowObj);
    }

    As you can see, a large number of template expressions have appeared if. The code is more verbose. And it’s hard to follow the basic logic. In addition, the meaning nulldoes not tell us much. We don’t really know why the previous function call failed. We have to guess. We create an error message and call showError(). Such code is dirtier and more confusing.

    Look again at the exception handling version. It clearly separates the successful path of the program and the exception handling code. A branch try is a good way, but catch mistakes. All exception handling occurs in one place. And individual functions may report why they failed. All in all, this seems pretty sweet. I think that the majority considers the first example to be quite suitable. Why a different approach?

    Problems handling exception try ... catch


    This approach allows you to ignore these annoying mistakes. Unfortunately, try...catchdoing his job too well. You just throw an exception and move on. We can catch him later. And everyone intends to always put such blocks, really. But it is not always obvious where the error goes further. And the block is too easy to forget. And before you realize this, your application crashes.

    In addition, exceptions pollute the code. We will not discuss functional purity in detail here. But let's look at one small aspect of functional purity: referential transparency. A link-transparent function always returns the same result for a particular input. But for functions with exceptions, we cannot say that. They can throw an exception at any time instead of returning a value. This complicates the logic. But what if you find a win-win option - a clean way to handle errors?

    We come up with an alternative


    Pure functions always return a value (even if that value is missing). Therefore, our error handling code should assume that we always return a value. So, as a first attempt, what should I do if, on failure, we return an Error object? That is, wherever we make an error, we return such an object. It might look something like this:

    function processRowReturningErrors(headerFieldNames, row) {
        fields = splitFields(row);
        rowObj = zipRow(headerFieldNames, fields);
        if (rowObj instanceof Error) {
            return showError(rowObj);
        }
        rowObjWithDate = addDateStr(rowObj);
        if (rowObjWithDate instanceof Error) {
            return showError(rowObjWithDate);
        }
        return rowToMessage(rowObj);
    }

    This is not a special upgrade without exception. But it’s better. We have transferred responsibility for error messages back to individual functions. But we still have all these ifs. It would be nice to encapsulate the template somehow. In other words, if we know that we have a bug, do not worry about the rest of the code.

    Polymorphism


    How to do it? This is a difficult problem. But it can be solved with the help of the magic of polymorphism . If you have not encountered polymorphism before, do not worry. In essence, it is “providing a single interface for entities of different types” (Straustrup, B. “C ++ Glossary of Björn Straustrup”). In JavaScript, this means that we create objects with the same named methods and signatures. But different behavior. A classic example is application logging. We can send our magazines to different places depending on the environment in which we are. What if we create two logger objects, for example?

    const consoleLogger = {
        log: function log(msg) {
            console.log('This is the console logger, logging:', msg);
        }
    };
    const ajaxLogger = {
        log: function log(msg) {
            return fetch('https://example.com/logger', {method: 'POST', body: msg});
        }
    };

    Both objects define a log function that expects a single string parameter. But they behave differently. The beauty is that we can write code that calls .log(), no matter which object it uses. It could be consoleLoggeror ajaxLogger. Everything works anyway. For example, the code below will work equally well with any object:

    function log(logger, message) {
        logger.log(message);
    }

    Another example is a method .toString()for all JS objects. We can write a method .toString()for any class that we create. Next, you can create two classes that implement the method in different ways .toString(). We will name them Leftand Right(a little later I will explain the names).

    class Left {
      constructor(val) {
        this._val = val;
      }
      toString() {
        const str = this._val.toString();
        return `Left(${str})`;
      }
    }

    class Right {
      constructor(val) {
        this._val = val;
      }
      toString() {
        const str = this._val.toString();
        return `Right(${str})`;
      }
    }

    Now create a function that calls .toString()on these two objects:

    function trace(val) {
        console.log(val.toString());
        return val;
    }
    trace(new Left('Hello world'));
    // ⦘ Left(Hello world)
    trace(new Right('Hello world'));
    // ⦘ Right(Hello world);

    Not outstanding code, I know. But the fact is that we have two different types of behavior that use the same interface. This is polymorphism. But pay attention to something interesting. How many if statements did we use? Zero. No one. We created two different types of behavior without a single if statement. Perhaps something like this can be used to handle errors ...

    Left and Right


    Returning to our problem. It is necessary to determine the successful and unsuccessful path for our code. On a good path, we simply continue to calmly run the code until an error occurs or we finish it. If we find ourselves on the wrong track, we will no longer try to run the code. We could name these paths Happy and Sad, but try to follow the naming conventions that other programming languages ​​and libraries use. So, let's call the bad path Left, and the successful one - Right.

    Let's create a method that runs the function if we are on a good path, but ignore it on a bad one:

    /**
     * Left represents the sad path.
     */
    class Left {
        constructor(val) {
            this._val = val;
        }
        runFunctionOnlyOnHappyPath() {
            // Left is the sad path. Do nothing
        }
        toString() {
            const str = this._val.toString();
            return `Left(${str})`;
        }
    }

    /**
     * Right represents the happy path.
     */
    class Right {
      constructor(val) {
        this._val = val;
      }
      runFunctionOnlyOnHappyPath(fn) {
        return fn(this._val);
      }
      toString() {
        const str = this._val.toString();
        return `Right(${str})`;
      }
    }

    Something like this:

    const leftHello  = new Left('Hello world');
    const rightHello = new Right('Hello world');
    leftHello.runFunctionOnlyOnHappyPath(trace);
    // does nothing
    rightHello.runFunctionOnlyOnHappyPath(trace);
    // ⦘ Hello world
    // ← "Hello world"

    Broadcast


    We are approaching something useful, but not quite yet. Our method .runFunctionOnlyOnHappyPath()returns a property _val. Everything is fine, but too inconvenient if we want to run more than one function. Why? Because we no longer know whether we are on the right or wrong path. Information disappears as soon as we take the value outside Left and Right. So what we can do is return the Left or Right path with a new one _valinside. And we will shorten the name, since we are here. What we do is translate a function from the world of simple values ​​to the world of Left and Right. Therefore, we call the method map():

    /**
     * Left represents the sad path.
     */
    class Left {
        constructor(val) {
            this._val = val;
        }
        map() {
            // Left is the sad path
            // so we do nothing
            return this;
        }
        toString() {
            const str = this._val.toString();
            return `Left(${str})`;
        }
    }

    /**
     * Right represents the happy path
     */
    class Right {
        constructor(val) {
            this._val = val;
        }
        map(fn) {
            return new Right(
                fn(this._val)
            );
        }
        toString() {
            const str = this._val.toString();
            return `Right(${str})`;
        }
    }

    We insert this method and use Left or Right in the free syntax:

    const leftHello        = new Left('Hello world');
    const rightHello       = new Right('Hello world');
    const helloToGreetings = str => str.replace(/Hello/, 'Greetings,');
    leftHello.map(helloToGreetings).map(trace);
    // Doesn't print any thing to the console
    // ← Left(Hello world)
    rightHello.map(helloToGreetings).map(trace);
    // ⦘ Greetings, world
    // ← Right(Greetings, world)

    We have created two paths of execution. We can put the data on a successful path by calling new Right(), or on a failed path by calling new Left().


    Each class represents a path: successful or unsuccessful. I stole this railway metaphor from Scott Vlashchina.

    If it mapworked on a good path, we go along it and process the data. If we find ourselves unsuccessful, nothing will happen. Just keep passing the value further. If we, for example, put Error on this wrong path, we would get something very similar to try…catch.


    We use it .map()to move along the path.

    As the further path progresses, it becomes a little difficult to write Left or Right all the time, so we call this combination simply Either (“either”). Either left or right.

    Shortcuts for creating Either objects


    So, the next step is to rewrite our example functions so that they return Either. Left for error or Right for value. But before we do this, have some fun. Let's write a couple of shortcuts. The first is a static method called .of(). It just returns a new Left or Right. The code may look like this:

    Left.of = function of(x) {
        return new Left(x);
    };
    Right.of = function of(x) {
        return new Right(x);
    };

    Frankly, even Left.of()and Right.of()tedious to write. Therefore, I tend to have even shorter shortcuts left()and right():

    function left(x) {
        return Left.of(x);
    }
    function right(x) {
        return Right.of(x);
    }

    With these shortcuts, we begin to rewrite the application functions:

    function zipRow(headerFields, fieldData) {
        const lengthMatch = (headerFields.length == fieldData.length);
        return (!lengthMatch)
            ? left(new Error("Row has an unexpected number of fields"))
            : right(_.zipObject(headerFields, fieldData));
    }
    function addDateStr(messageObj) {
        const errMsg = 'Unable to parse date stamp in message object';
        const months = [
            'January', 'February', 'March', 'April', 'May', 'June', 'July',
            'August', 'September', 'October', 'November', 'December'
        ];
        const d = new Date(messageObj.datestamp);
        if (isNaN(d)) { return left(new Error(errMsg));  }
        const datestr = `${d.getDate()} ${months[d.getMonth()]} ${d.getFullYear()}`;
        return right({datestr, ...messageObj});
    }

    Modified functions are not so different from the old ones. We simply wrap the return value in either Left or Right, depending on whether there is an error.

    After that, we can start processing the main function that processes one line. To begin with, put the string in Either s right(), and then translate splitFieldsto split it:

    function processRow(headerFields, row) {
        const fieldsEither   = right(row).map(splitFields);
       // …
    }

    This works just fine, but trouble happens if you try to do the same with zipRow():

        function processRow(headerFields, row) {
            const fieldsEither   = right(row).map(splitFields);
            const rowObj         = fieldsEither.map(zipRow /* wait. this isn't right */);
            // ...
        }

    The fact is that it zipRow()expects two parameters. But the functions we pass in .map()get only one value from the property ._val. The situation can be corrected using the curried version zipRow(). It might look something like this:

    function zipRow(headerFields) {
        return function zipRowWithHeaderFields(fieldData) {
            const lengthMatch = (headerFields.length == fieldData.length);
            return (!lengthMatch)
                ? left(new Error("Row has an unexpected number of fields"))
                : right(_.zipObject(headerFields, fieldData));
        };
    }

    This small change simplifies the conversion zipRow, so it will work well with .map():

    function processRow(headerFields, row) {
        const fieldsEither   = right(row).map(splitFields);
        const rowObj         = fieldsEither.map(zipRow(headerFields));
        // ... But now we have another problem ...
    }

    Join


    Use .map()for launch splitFields() is normal as it .splitFields()does not return Either. But when you have to start zipRow(), a problem arises because it returns Either. So when used, .map()we end up bumping into Either inside Either. If we go further, then get stuck until we run .map()inside .map(). That won't work either. We need some way to combine these nested Either. So let's write a new method, which we will call .join():

    /**
     *Left represents the sad path.
     */
    class Left {
        constructor(val) {
            this._val = val;
        }
        map() {
            // Left is the sad path
            // so we do nothing
            return this;
        }
        join() {
            // On the sad path, we don't
            // do anything with join
            return this;
        }
        toString() {
            const str = this._val.toString();
            return `Left(${str})`;
        }
    }

    /**
     * Right represents the happy path
     */
    class Right {
        constructor(val) {
            this._val = val;
        }
        map(fn) {
            return new Right(
                fn(this._val)
            );
        }
        join() {
            if ((this._val instanceof Left)
                || (this._val instanceof Right))
            {
                return this._val;
            }
            return this;
        }
        toString() {
            const str = this._val.toString();
            return `Right(${str})`;
        }
    }

    Now we can “unpack” our assets:

    function processRow(headerFields, row) {
        const fieldsEither   = right(row).map(splitFields);
        const rowObj         = fieldsEither.map(zipRow(headerFields)).join();
        const rowObjWithDate = rowObj.map(addDateStr).join();
        // Slowly getting better... but what do we return?
    }

    Chain


    We have come a long way. But you have to remember the challenge all the time .join(), which is annoying. However, we do have a common consecutive call pattern .map()and .join(), so let's create a quick access method for it. We will call it chain()(chain) because it links together functions that return Left or Right.

    /**
     *Left represents the sad path.
     */
    class Left {
        constructor(val) {
            this._val = val;
        }
        map() {
            // Left is the sad path
            // so we do nothing
            return this;
        }
        join() {
            // On the sad path, we don't
            // do anything with join
            return this;
        }
        chain() {
            // Boring sad path,
            // do nothing.
            return this;
        }
        toString() {
            const str = this._val.toString();
            return `Left(${str})`;
        }
    }

    /**
     * Right represents the happy path
     */
    class Right {
        constructor(val) {
            this._val = val;
        }
        map(fn) {
            return new Right(
                fn(this._val)
            );
        }
        join() {
            if ((this._val instanceof Left)
                || (this._val instanceof Right)) {
                return this._val;
            }
            return this;
        }
        chain(fn) {
            return fn(this._val);
        }
        toString() {
            const str = this._val.toString();
            return `Right(${str})`;
        }
    }

    Returning to the analogy with railroad tracks, he .chain()switches the rails if we encounter an error. However, it is easier to show on the diagram.


    If an error occurs, the .chain () method allows you to switch to the left path. Please note that the switches only work in one direction.

    The code has become a little cleaner:

    function processRow(headerFields, row) {
        const fieldsEither   = right(row).map(splitFields);
        const rowObj         = fieldsEither.chain(zipRow(headerFields));
        const rowObjWithDate = rowObj.chain(addDateStr);
        // Slowly getting better... but what do we return?
    }

    Do something with values


    Function refactoring is processRow()almost complete. But what happens when we return the value? In the end, we want to take different actions depending on what kind of situation we have: Left or Right. Therefore, we will write a function that will take appropriate measures:

    function either(leftFunc, rightFunc, e) {
        return (e instanceof Left) ? leftFunc(e._val) : rightFunc(e._val);
    }

    I cheated and used the internal values ​​of Left or Right objects. But pretend that you did not notice this. Now we can complete our function:

    function processRow(headerFields, row) {
        const fieldsEither   = right(row).map(splitFields);
        const rowObj         = fieldsEither.chain(zipRow(headerFields));
        const rowObjWithDate = rowObj.chain(addDateStr);
        return either(showError, rowToMessage, rowObjWithDate);
    }

    And if we feel particularly smart, then we can again use the free syntax:

    function processRow(headerFields, row) {
        const rowObjWithDate = right(row)
            .map(splitFields)
            .chain(zipRow(headerFields))
            .chain(addDateStr);
        return either(showError, rowToMessage, rowObjWithDate);
    }

    Both versions are pretty pretty. No designs try...catch. And no if-statements in the top-level function. If there is a problem with a particular line, we simply display an error message at the end. And note that in processRow()we mention Left or Right the only time at the very beginning when we call right(). The rest are only used methods .map()and .chain()for use the next function.

    ap and lift


    It looks good, but it remains to consider one last scenario. Following our example, let's see how it is possible to process all CSV data, and not just each row individually. We will need an auxiliary function (helper) or three:

    function splitCSVToRows(csvData) {
        // There should always be a header row... so if there's no
        // newline character, something is wrong.
        return (csvData.indexOf('\n') < 0)
            ? left('No header row found in CSV data')
            : right(csvData.split('\n'));
    }
    function processRows(headerFields, dataRows) {
        // Note this is Array map, not Either map.
        return dataRows.map(row => processRow(headerFields, row));
    }
    function showMessages(messages) {
        return `
      ${messages.join('\n')}
    `; }

    So, we have a helper that breaks CSV into lines. And we return to the option with Either. Now you can use .map()some lodash functions to extract the title bar from data lines. But we find ourselves in an interesting situation ...

    function csvToMessages(csvData) {
        const csvRows      = splitCSVToRows(csvData);
        const headerFields = csvRows.map(_.head).map(splitFields);
        const dataRows     = csvRows.map(_.tail);
        // What’s next?
    }

    We have header fields and data rows ready to display with processRows(). But headerFieldsalso dataRowswrapped in Either. We need some way to convert processRows()to a function that works with Either. To begin, we carry out currying processRows.

    function processRows(headerFields) {
        return function processRowsWithHeaderFields(dataRows) {
            // Note this is Array map, not Either map.
            return dataRows.map(row => processRow(headerFields, row));
        };
    }

    Now everything is ready for the experiment. We have headerFields, which is Either, wrapped around an array. What will happen if we take headerFieldsand call on it .map()with processRows()?

    function csvToMessages(csvData) {
        const csvRows      = splitCSVToRows(csvData);
        const headerFields = csvRows.map(_.head).map(splitFields);
        const dataRows     = csvRows.map(_.tail);
        // How will we pass headerFields and dataRows to
        // processRows() ?
        const funcInEither = headerFields.map(processRows);
    }

    With .map (), an external function is called here processRows(), but not an internal one. In other words, processRows()returns a function. And since that .map(), we still get Either back. Thus, the result is a function inside Either, which is called funcInEither. It takes an array of strings and returns an array of other strings. We need to somehow take this function and call it with a value inside dataRows. To do this, add another method to our classes Left and Right. We will call it .ap()in accordance with the standard .

    As usual, the method does nothing on the Left track:

        // In Left (the sad path)
        ap() {
            return this;
        }

    And for the Right class, we expect another Either with a function:

        // In Right (the happy path)
        ap(otherEither) {
            const functionToRun = otherEither._val;
            return this.map(functionToRun);
        }

    Now we can complete our main function:

        function csvToMessages(csvData) {
            const csvRows      = splitCSVToRows(csvData);
            const headerFields = csvRows.map(_.head).map(splitFields);
            const dataRows     = csvRows.map(_.tail);
            const funcInEither = headerFields.map(processRows);
            const messagesArr  = dataRows.ap(funcInEither);
            return either(showError, showMessages, messagesArr);
        }

    The essence of the method is .ap()immediately understood a little (the specifications of Fantasy Land confuse it, but in most other languages ​​the method is used vice versa). If you describe it easier, then you say: “I have a function that usually takes two simple values. I want to turn it into a function that takes two Either. " If available, .ap()we can write a function that will do just that. Let's call it liftA2(), again in accordance with the standard name. She takes a simple function that expects two arguments, and “lift” it to work with “applicatives”. (these are objects that contain both a method .ap()and a method .of()). So, liftA2 is short for “applicative lift, two parameters”.

    So a function liftA2might look something like this:

    function liftA2(func) {
        return function runApplicativeFunc(a, b) {
            return b.ap(a.map(func));
        };
    }

    Our top-level function will use it as follows:

    function csvToMessages(csvData) {
        const csvRows      = splitCSVToRows(csvData);
        const headerFields = csvRows.map(_.head).map(splitFields);
        const dataRows     = csvRows.map(_.tail);
        const processRowsA = liftA2(processRows);
        const messagesArr  = processRowsA(headerFields, dataRows);
        return either(showError, showMessages, messagesArr);
    }

    Code on CodePen .

    True? It's all?


    You may ask, what is better than simple exceptions? Doesn’t it seem to me that this is too complicated a way to solve a simple problem? Let's first think about why we like exceptions. If there were no exceptions, you would have to write a lot of if-statements everywhere. We will always write code according to the principle "if the latter works, continue, otherwise process the error." And we must handle these errors throughout the code. This makes it difficult to understand what is happening. Exceptions allow you to exit the program if something went wrong. Therefore, you do not need to write all these ifs. You can focus on a successful path of execution.

    But there is one snag. Exceptions hide too much. When you throw an exception, you transfer the error handling problem to some other function. It is too easy to ignore an exception that will pop up to the highest level. The nice side of Either is that it allows you to jump out of the main program stream, as if with an exception. And it works honestly. You get either Right or Left. You cannot pretend that the Left option is impossible. In the end, you have to pull the value out with a call like either().

    I know it sounds like some kind of complexity. But take a look at the code we wrote (not classes, but functions that use them). There is not much exception handling code. It is almost absent, except for a call either()at the end csvToMessages()andprocessRow(). That's the whole point. With Either, you have clean error handling that cannot be accidentally forgotten. Without Either, stamp through the code and add padding everywhere.

    This does not mean that you should never use it try...catch. Sometimes it’s the right tool, and it’s normal. But this is not the only tool. Either gives you some benefits that u don't have try...catch. So give a chance to this monad. Even if it’s difficult at first, I think you will like it. Please, please do not use the implementation from this article. Try one of the famous libraries such as Crocks , Sanctuary , Folktale or Monet . They are better served. And here, for simplicity, I missed something.

    Additional Resources



    Also popular now: