Skip to main content

Momento Node.js Style Guide

When writing Momento Node.js code, you can choose between two different styles: the pattern matching style, or the simplified style. This guide will help you understand the differences between the two styles, so that you can choose which one will work best for your project.

Pattern matching style

If you're not familiar with pattern matching, it is a mechanism that has become increasingly popular in modern programming languages. It allows you to match a value against a pattern to determine the type of the value, and then extract its properties. It's very useful when making network requests, where the type of the response can vary a great deal depending on whether the request was successful or not, because it gives you a way to write exhaustive, type-safe code that handles all possible response types.

For example, when you issue a Momento get request (or any other cache read request), the response can be one of three types:

  • Hit - the key was found in the cache, and the value was returned
  • Miss - the key was not found in the cache, so no value was returned
  • Error - an error occurred while trying to read the value from the cache

As you can imagine, a type-safe model of these different cases will expose very different properties depending on which type of response you recieve:

  • A Hit response will have a value property - and it is guaranteed not to be undefined, so you don't have to write any if statements to handle the undefined case.
  • A Miss response will have no properties because no value was returned.
  • An Error response will have an errorCode property, indicating what type of error occurred.

Using pattern matching, you can write code like this:

const result = await cacheClient.get('test-cache', 'test-key');
if (result instanceof CacheGet.Hit) {
console.log(`Retrieved value for key 'test-key': ${result.valueString()}`);
} else if (result instanceof CacheGet.Miss) {
console.log("Key 'test-key' was not found in cache 'test-cache'");
} else if (result instanceof CacheGet.Error) {
throw new Error(
`An error occurred while attempting to get key 'test-key' from cache 'test-cache': ${result.errorCode()}: ${result.toString()}`
);
}

In each branch of the if statement, the TypeScript compiler is smart enough to recognize that it now knows the exact type of the result object, so it will give you access to the correct properties based on which type it is. Because of this, you can catch many types of errors at compile time rather than finding out about them at run time. It also gives you a safer way to interact with the error objects than a normal try/catch block would.

If your primary goal is to write thorough, production-ready code with exhaustive handling of each different type of response you might receive for a request, then this pattern matching style is the way to go.

However, it can be more verbose and take longer to write than code you might be used to writing against other client libraries. If you prefer something more succinct, you might want to consider the simplified style.

Simplified style

With the simplified style, you won't be doing any pattern matching or type checking. Instead, you will simply call the .value() method on the response object. For the case of a Hit, you will get the value back; otherwise you will get back an undefined.

When using the simplified style you will probably want to enable the withThrowOnErrors setting. By default, Momento always returns errors as part of the return value, as opposed to throwing them. But when using the simplified code style, if you call .value() on a response and you get back undefined, you won't be able to tell if the response was a Miss or an Error. By enabling withThrowOnErrors, you can tell the Momento client that you prefer for it to actually throw the errors when they occur.

Here's how to enable withThrowOnErrors:

const cacheClient = await CacheClient.create({
configuration: Configurations.Lambda.latest().withThrowOnErrors(true),
credentialProvider: CredentialProvider.fromEnvVar('MOMENTO_API_KEY'),
defaultTtlSeconds: 60,
});

For more information on this topic, see the Configuration and Error Handling page.

Once you've enabled withThrowOnErrors, you can write code like this:

const result = (await cacheClient.get('test-cache', 'test-key')).value();
if (result !== undefined) {
console.log(`Retrieved value for key 'test-key': ${result}`);
} else {
console.log("Key 'test-key' was not found in cache 'test-cache'");
}

And if an error occurs, it will be thrown as an exception. You can catch the exception and handle it with a normal try/catch block, like this:

try {
const result = (await cacheClient.get('test-cache', 'test-key')).value();
if (result !== undefined) {
console.log(`Retrieved value for key 'test-key': ${result}`);
} else {
console.log("Key 'test-key' was not found in cache 'test-cache'");
}
} catch (e) {
const momentoError = e as SdkError;
if (momentoError.errorCode() === MomentoErrorCode.LIMIT_EXCEEDED_ERROR) {
console.log('Request rate limit exceeded, may need to request a limit increase!');
} else {
throw new Error(
`An error occurred while attempting to get key 'test-key' from cache 'test-cache': ${momentoError.errorCode()}: ${momentoError.toString()}`
);
}
}

The simplified style probably looks and feels more familiar to you compared to other Node.js client libraries you might have used in the past. It's also more succinct and faster to write than the pattern matching style. If you prefer to write code that is more concise, and you don't mind the trade-off of not having exhaustive type checking, then this style may be the right fit for you!