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 returnedMiss
- the key was not found in the cache, so no value was returnedError
- 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 avalue
property - and it is guaranteed not to beundefined
, so you don't have to write anyif
statements to handle theundefined
case. - A
Miss
response will have no properties because no value was returned. - An
Error
response will have anerrorCode
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');
switch (result.type) {
case CacheGetResponse.Hit:
console.log(`Retrieved value for key 'test-key': ${result.valueString()}`);
break;
case CacheGetResponse.Miss:
console.log("Key 'test-key' was not found in cache 'test-cache'");
break;
case CacheGetResponse.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!