Support for custom `error` catcher for api
nechaido opened this issue · 10 comments
Describe the problem
It is often not enough to catch any error that arises in the application code and to send error 500 to the client.
Describe the solution
Better solution would be to allow custom API handler wrappers to be provided
Alternatives
No response
Additional context
Concerning errors: I think we can use single Error class with following features:
export interface ErrorOptions {
code?: number | string;
cause?: Error;
}
export class Error extends global.Error {
constructor(message: string, options?: number | string | ErrorOptions);
message: string;
stack: string;
code?: number | string;
cause?: Error;
}
Another option:
class DomainError extends Error {
...
}
class MessengerError extends DomainError {
...
static invalidPermission() {
return new MessengerError('Permission denied');
}
static notFound() {
return new MessengerError('Not found');
}
// and so on...
}
This neither requires defining a class per each error (1 proposal) nor requires additional functionality to check if a general purpose error received an allowed message (2 proposal).
This also adds static checks and autocompletion.
I suggest to have onError
handler for API method like this:
({
parameters: {
a: 'number',
b: 'number',
},
method: async ({ a, b }) => {
const result = a + b;
return result;
},
onError: (error) =>
'code' in error ? error : { message: error.message, code: 500 },
returns: 'number',
});
The goal is to have any custom error handlers in case if API method failed.
Inspired from Fastify hooks
https://www.fastify.io/docs/latest/Reference/Hooks/#onerror
https://www.fastify.io/docs/latest/Reference/Lifecycle/
It is just a draft proposal/example of how we can declare a list of the errors with TypeScript. data: any
can be typed better.
export enum ErrorCode {
notFound = 'NOT_FOUND',
invalidResponse = 'INVALID_RESPONSE',
authenticationError = 'AUTHENTICATION_ERROR',
networkError = 'NETWORK_ERROR',
}
export type ErrorOptions = {
message: string;
data?: any;
};
const record: Record<ErrorCode, ErrorOptions> = {
NOT_FOUND: { message: 'Not found' },
INVALID_RESPONSE: {
message: 'Invalid response. Please try again later.',
data: { additionalFields: 'something' },
},
AUTHENTICATION_ERROR: {
message: 'Authentication failed',
},
NETWORK_ERROR: {
message: 'Network error occurred',
},
};
export const createError = (code: ErrorCode) => ({ code, ...record[code] });
console.log(createError(ErrorCode.notFound));
// error names
const DOMAIN_ERROR_NAME = {
NotFound = 'NotFound',
Forbidden = 'Forbidden',
SomeEntityIsDeactivated = 'SomeEntityIsDeactivated',
...
};
// domain layer
throw new DomainError(DOMAIN_ERROR_NAME.NotFound)
export interface LogFormatError {
toJSON(): Record<string, unknown>;
// compatible with https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#tojson_behavior.
}
export interface UserFormatError {
toUserError(): Record<string, unknown>;
// allows to provide custom data when error is sent to user
}
both interfaces are optional for all Errors. Logger/API will check for those upon handling.
Endpoint code:
({
parameters: {
person: { domain: 'Person' },
address: { domain: 'Address' },
},
method: async ({ person, address }) => {
const addressId = await api.gs.create(address);
person.address = addressId;
const personId = await api.gs.create(person);
return personId;
},
returns: { type: 'number' },
errors: {
ECONTRACT: 'Invalid arguments',
ECANTSAVE: 'Person and address can not be saved to database',
ERESULT: 'Invalid result',
},
});
We can generate example.d.ts
automaticaly
type Code = 'ECONTRACT' | 'ECANTSAVE' | 'ERESULT';
class CustomError {
constructor(message: string, options: { code: Code });
}
I believe, we need complex examples, not just a code snippet, we need branch/fork with a few files to show how it will work and ready to try branch, for example my proposal: metarhia/Example#233
@lundibundi @nechaido @mprudnik @georgolden @DemianParkhomenko @Haliont
Added my extended proposal - metarhia/Example#234.
Additional notes regarding error hooks:
I propose to have multiple places, where onError
handler can be defined - in method itself (near handler) and error.js
file on service level. Then we can trigger the nearest one in case of error:
- if method defines own
onError
- use it and ignore service and global error handler - else if service defines own
onError
- use it and ignore global error handler - else use global error handler.
Also, I'm not sure if those handlers should be triggered if error
is an instance of DomainError
since normally that means that it is part of expected behavior and the error can be serialized and sent to the client.
Implemented in Version 3.0.0