Flexible and explicit error handling with a small flavor of functional languages.
Result type allows you go from this:
const user = getUser(email);
if (!user) {
throw new Error("Can't get user");
// but what happens, why email is invalid?
// and who will catch this error?
}
to this:
const userResult = getUser(email);
if (userResult.isErr()) {
const err = userResult.err();
switch (err.type) {
case 'InvalidEmail':
console.log('User email is invalid');
break;
case 'UserNotExists':
console.log("Can't find the user");
break;
case 'UserBannded':
console.log(`User was bannded: ${err.ban}`);
break;
}
}
There are two main function for creating results ok
and fail
.
import { Err, Result, ok, fail } from 'lambda-res';
const success = ok('good'); // inferred as Success<string>
const error = fail<Err>('bad'); // inferred as Failure<string>
const result: Result<string, Err> = Math.random() ? success : error;
Err
used for creating errors of given type.
import { Failure, Err, fail, nope } from 'lambda-res';
// same as: { type: 'AppDatabaseError' } & Err
type DatabaseError = Err<'AppDatabaseError'>;
type ConnectionError = Err<'AppConnectionError'>;
type AppErrors = DatabaseError | ConnectionError; // | Err<'NotHandled'>;
const check = (err: AppErrors): string => {
if (err.type === 'AppDatabaseError') {
return 'Database error';
}
if (err.type === 'AppConnectionError') {
return 'Connection error';
}
// try unncoment NotHandled error and typescript will show a problem here!
// that ensures that all error types are checked exhaustively
nope(err);
};
// must match the types or Typescript will show an error
const failure: Failure<AppErrors> = fail<DatabaseError>('AppDatabaseError');
check(failure.err()); // get en error from failure
Errs
type can be used to create multiple errors of a given kind, which may be
useful in order not to pollute your exports with multiple individual errors
(like: DatabaseError, ConnectionError
).
import { Failure, Errs, fail } from 'lambda-res';
// only AppError needs to be exported
type AppError = Errs<{
name: 'App';
DatabaseError: string;
ConnectionError: string;
}>;
// note an Errs here (inside Failure):
// it transform AppError into Err<'AppDatabaseError'> | Err<'AppConnectionError'>
const failure: Failure<Errs<AppError>> = fail<AppError['DatabaseError']>('AppDatabaseError');
Once you created result it have a bunch of useful methods:
import { Err, Result, ok, fail } from 'lambda-res';
const result: Result<string, Err> = Math.random() ? ok('ok') : fail<Err<'Error1'>>('Error1');
// isOk method allows to narrow result type to Success<string>
if (result.isOk()) {
// once type is narrowed you can safely access the data
console.log(result.ok()); // will outputs 'ok'
}
// isErr method allows to narrow result type to Failure<Err<'Error1'>>
if (result.isErr()) {
console.log(result.err().type);
}
// result.err();
// ^
// this will throw an exception because result is not a Failure
// that is why isErr check used first
import { Response, Errs, ok, fail, nope } from 'lambda-res';
type GetUserEmailError = Errs<{
name: 'GetUserEmail';
NotFound: string;
Deleted: string;
}>;
const getUserName = async (id: string): Response<string, Errs<GetUserEmailError>> => {
if (id === 'id1') {
return ok('[email protected]');
} else if (id === 'id2') {
return fail('GetUserEmailDeleted');
}
return fail('GetUserEmailNotFound');
};
type SendEmailError = Errs<{
name: 'SendEmail';
DeliveryError: string;
}>;
const sendEmail = async (email: string): Response<{ sentCount: number }, Errs<SendEmailError>> => {
if (email === '[email protected]') {
return ok({ sentCount: 3 })
}
return fail('SendEmailDeliveryError')
}
const sendEmailToUser = async () /*: Response<{ sentCount: number }, Err<'DeliveryError'> | Err<'LoadingUserError'>> */ => {
const userNameRes = await getUserName('id1');
return userNameRes.onOk(async (email) => {
const sendRes = await sendEmail(email);
return sendRes;
}).onErr((err) => {
if (err.type === 'SendEmailDeliveryError') {
return { type: 'DeliveryError' }
} else if (err.type === 'GetUserEmailDeleted' || err.type === 'GetUserEmailNotFound') {
return { type: 'LoadingUserError' }
}
nope(err);
}).res();
}
On success return new Data2
and collects all errors. Used to sequentially run multiple functions
which returns result. In the end onErr
can map errors to desired error type.
Params:
-
cb
: callback which receivesdata
:Data
res
:Result<Data, never>
-
name
(optional) : common error name, it will prepend all errors types
Returns:
Result<Data2, Fail | Fail2>
orResponse<Data2, Fail | Fail2>
(for async functions)
Doesn't change Data
and returns new Fail2
— used to map one type of errors to another.
Params:
cb
: callback which receiveserr
:Fail
res
:Result<never, Fail>
Returns:
onErr
:string
,{ type: string }
orPromise<string>
,Promise<{ type: string }>
onFail
:Result<never, Fail2>
orResponse<never, Fail2>