Error Handling Development Guide (Basic)
This guide highlights the main aspects of error handling in a Mosaic backend service.
The three main points of the error handling are throwing new errors, catching thrown errors, and mapping caught errors to something more user-friendly.
The Mosaic library @axinom/mosaic-service-common
provides all required
components to simplify these points.
Throwing errors
When throwing errors, the default Error
class could work in some cases, but it
does not provide an option to store extra information like error codes and other
details in a convenient way. It’s encouraged to use the MosaicError
instead. E.g.
import { MosaicError } from '@axinom/mosaic-service-common';
throw new MosaicError({
message: 'This is an error message.',
code: 'THIS_IS_AN_ERROR_CODE',
});
The table below shows all parameters that MosaicError expects:
parameter | Is Required? | Description |
---|---|---|
message | Yes | A human-readable error message. |
code | Yes | An error code, ideally all capital letters, separated with underscores. |
error | No | An instance of an original error. It will be referenced in the resulting log. |
details | No | Additional details to add to the error. These details will be present in the GraphQL API Response and in the resulting log when using Mosaic default settings. |
logInfo | No | Additional information to add to the error to be logged. This info will NOT be present in the GraphQL API Response when using Mosaic default settings. |
messageParams | No | An array of parameter to be used for printf-style message placeholders. |
Catching errors
When an error is caught in a try/catch block, it has the unknown
type (or any
, if
typescript is configured to not use strict
mode), and you would usually want
to assert that thrown error is indeed an instance of an Error
class, and then
check if it should be mapped to a more user-friendly error or not.
The simplest approach is to use getMappedError
function:
import { getMappedError } from '@axinom/mosaic-service-common';
try {
// code that has potential to throw an error
} catch (error) {
throw getMappedError(error);
}
This function would run the error through several checks:
- If the thrown error is
MosaicError
orNonMosaicError
- it’s re-returned as is.NonMosaicError
is a dedicated wrapper for unhandled errors.
- If the thrown error is not an instance of an
Error
class -NonMosaicError
is returned (wrapping the original error). - The error is mapped using a default
defaultPgErrorMapper
which maps PostgreSQL error codes to a human-readable message and error codes.- If the mapper returns some info - a MosaicError is created using that info
- If the mapper returns a falsy value - a
NonMosaicError
is returned.
Mapping caught errors
When getMappedError
is not enough and you need to map an error based on some
custom conditions, e.g. based on thrown error properties, you can create your
own getter function, which would utilize the same checks as getMappedError
,
but would use a custom mapping function instead of (or in addition to) the
defaultPgErrorMapper
. E.g.
import {
defaultPgErrorMapper,
MosaicErrorInfo,
mosaicErrorMappingFactory,
} from '@axinom/mosaic-service-common';
const getCustomMappedError = mosaicErrorMappingFactory(
(error: Error, defaultError?: MosaicErrorInfo) => {
if (error.name === 'SomeSpecificName') {
return {
message: 'This is an error message',
code: 'THIS_IS_AN_ERROR_CODE',
};
}
return defaultPgErrorMapper(error, defaultError);
},
);
try {
// code that has the potential to throw an error
} catch (error) {
throw getCustomMappedError(error);
}
The mosaicErrorMappingFactory
expects a mapping function that will receive an
error with an optional generic context object and will return a MosaicErrorInfo
, which
is the same object that MosaicError
expects, or undefined
- meaning that
mapping has failed and an unhandled error must be thrown (NonMosaicError).
Custom mapping happens after an Error
is checked if it’s an instance of an
Error
or not, that’s why mapping function expects Error
instead of the
unknown
. If you know that your code can throw strings or non Error based objects - it’s
advised to transform them into an appropriate error before getMappedError
or
getCustomMappedError
is called. It’s always expected that if something is
thrown - it’s an instance of an Error. And if it’s not - it’s most probably a
bug that should be fixed.
Keeping errors organized
In the examples above, we were using explicit objects with message
and code
properties to pass to MosaicError
or to the custom mapper, but it’s better to
store them in a dedicated object. E.g.
export const CommonErrors = {
SomethingFailed: {
message: `Some type of error has occurred.`,
code: 'SOMETHING_FAILED',
},
OtherThingFailed: {
message: `Another type of error has occurred.`,
code: 'OTHER_THING_FAILED',
},
} as const;
Then, in your code, you would use this custom object instead of the explicit object. E.g.
import { MosaicError } from '@axinom/mosaic-service-common';
import { CommonErrors } from '../common';
throw new MosaicError(CommonErrors.SomethingFailed);
One additional reason to keep all available error codes and messages in a
centralized location is that they can be exposed as a GraphQL ErrorCodesEnum
using a dedicated AddErrorCodesEnumPluginFactory
, which can then be used by frontends. E.g.
import { IdGuardErrors } from '@axinom/mosaic-id-guard';
import { AddErrorCodesEnumPluginFactory, MosaicErrors, PostgraphileOptionsBuilder } from '@axinom/mosaic-service-common';
import { CommonErrors } from '../common';
new PostgraphileOptionsBuilder(config.isDev, config.graphqlGuiEnabled)
.addPlugins(
AxGuardPlugin,
EnforceStrictPermissionsPlugin,
AnnotateTypesWithPermissionsPlugin,
AtomicMutationsPlugin,
AddErrorCodesEnumPluginFactory([
MosaicErrors,
IdGuardErrors,
CommonErrors,
]),
)