Skip to main content

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:

parameterIs Required?Description
messageYesA human-readable error message.
codeYesAn error code, ideally all capital letters, separated with underscores.
errorNoAn instance of an original error. It will be referenced in the resulting log.
detailsNoAdditional 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.
logInfoNoAdditional 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.
messageParamsNoAn 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 or NonMosaicError - 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).

note

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,
]),
)