Skip to content

Repository files navigation

okej

okej a bare-metal implemention of Result types in JavaScript.

This library introduces some super simple helper functions so that data and errors can easily be passed throughout an application without the need to add try/catch statements everywhere.

See for yourself:

constresult=doSomething();// returns ok() or err()if(result.ok){// yes we have an Ok value. Let's examine the dataconsole.log(result.data);}else{// no looks like we have an Err valueconsole.log(result.errMessage);// something went wrongconsole.log(result.errCode);// figure out what to do based on the code value}

What are Result objects?

A Result object is simply an object that wraps itself around data as well as error information. Result objects exist in Rust, Elm, Kotlin, etc..., but not in JavaScript.

Here's an example of how they work:

import{err,ok}from"okej";// Ok result (when something is valid)constgoodResult=ok("data");// Err result (when something invalid or fails)constbadResult=err("something failed","ERROR_500");

Now for any code that deals with a result object, it must deal with the result object and explicity deal with it's success and failure (ok/err) states.

// result is either `goodResult` or `badResult`if(result.ok){// valid state (continue along with the happy path)}else{// invalid state (deal with the error)}

Result objects are useful for the following reasons:

  • They force the developer to plan out each error and figure out how to handle them
  • Result objects can be returned up the chain to other functions
  • All Result objects have an error code as well as an error message
  • They clearly separate system/runtime errors from logic errors
  • try/catch statements can be used for truly unexpected situations (i.e. network errors or actual code issues)

Want more information? Click here for more

License

MIT.

Installation

First install it into your project.

# via NPM
npm install okej
# or Yarn
yarn add okej
# or PNPM
pnpm add okej

Then import it as needed:

// ESM / MJSimport{err,ok,Result}from"okej";// CommonJS / old school Nodeconst{ err, ok, Result }=require("okej");

Usage

Below is a example of how data is processed

import{err,ok,Result}from"okej";// let's imagine this function is calling some remote operation// and will return either a Ok or Err result once complete.asyncfunctionokOrErr(): Promise<Result>{return(awaitoperationSucceeds())
? ok({message: "yes!"})
: err("didn't work out today",500);}// we can check the data that is returnedconstresult=awaitokOrErr();if(result.ok){console.log(result.data);// { message: "yes!" }}else{console.log(result.errMessage);// didn\'t work out todayconsole.log(result.errCode);// 500}

Docs

The main functions are ok() and err(), but there are also various helper functions that can be used as well.

ok()

When called, a Result object with ok=true/err=false will be returned.

import{ok}from"okej";// with no dataletokResult=ok();console.log(okResult.ok);// trueconsole.log(okResult.err);// false// with dataokResult=ok({id: "123",name: "foo"});console.log(okResult.ok);// trueconsole.log(okResult.err);// falseconsole.log(okResult.data.id);// "123"console.log(okResult.data.name);// "foo"

err()

When called, a Result object with ok=false/err=true will be returned.

import{err}from"okej";// with no dataletresult=err();console.log(result.ok);// falseconsole.log(result.err);// true// with dataresult=err({errCode: 123,errMessage: "something broke",context: {customDataRelatedToError: "cool"},});console.log(result.ok);// falseconsole.log(result.err);// trueconsole.log(result.errCode);// 123console.log(result.errMessage);// "something broke"console.log(result.errContext);// { customDataRelatedToError: "cool" }// by other means...result=err("something broke");// result.errMessage is "something broke"result=err("something broke",10);// result.errMessage/result.errCode setresult=err("something broke","BAD ERROR");// yes string-based error codes work tooresult=err("something broke",10,{extraData: true});// result.errMessage/result.errCode/result.errContext set

We can also pass in a JavaScript Error instance directly:

import{err}from"okej";try{//...}catch(e){result=err(e,{errCode: 10});console.log(result.ok);// falseconsole.log(result.errCode);// 10 (default is 0)console.log(result.errMessage);// (will be inferred from the `e` param)console.log(result.errContext);// nullconsole.log(result.errException);// the provided `e` value}

When TypeScirpt is used, err() can be used to deal with error code enums:

constenumHttpError{BadRequest=400,NotFound=404,ServerError=500,Forbidden=403,}interfaceApiData{id: string;totalUsers: number;userNames: string[];}asyncfunctioncallApiUsersServer(url: string,): Promise<Result<ApiData,HttpError>>{// call the api server and deal with the errortry{constreq=awaitfetch(url);if(response.status==500)returnerr("server error",HttpError.ServerError);if(response.status==403)returnerr("forbidden",HttpError.Forbidden);if(response.status==404)returnerr("not found",HttpError.NotFound);constjson=awaitreq.json();if(!json||!isValidResponse(json)){// this might be a 404 in the real world... This is just an example!returnerr("bad request",HttpError.BadRequest);}returnok(json);}catch(e){returnerr("bad request",HttpError.BadRequest);}}constrequest=awaitcallApiUsersServer();if(request.err){// deal with the error (TypeScript will know that `errCode` is an instance of HttpError)switch(request.errCode){caseHttpError.BadRequest:
// handle the bad request errorbreak;caseHttpError.NotFound:
// handle the not found errorbreak;caseHttpError.ServerError:
// handle the not server errorbreak;caseHttpError.Forbidden:
// handle the not forbidden errorbreak;default:
// just incase a new error was introduced laterbreak;}}

Helper Functions

The helper functions below can be used to deal with collections of result data.

import{allErr,allOk,isResult,merge}from"okej";// true if all have ok=true/err=false (false if an empty array)allOk(results);// true if all have ok=false/err=true (false if an empty array)allErr(results);// true if some have ok=true/err=falsesomeOk(results);// true if some have ok=false/err=truesomeErr(results);

There also some functions that check to see if a result is indeed a result. Note that each of the functions listed in the code example below adhere to TypeScript type narrowing as well as assertions.

import{allErr,allOk,isResult,merge}from"okej";// true if Ok or ErrisResult(someValue);// true if OkisOkResult(someValue);// true if ErrisErrResult(someValue);// throws an error if not a Ok or Err valueassertIsResult(someValue);// throws an error if not a Ok valueassertIsOkResult(someValue);// throws an error if not a Err valueassertIsErrResult(someValue);

When dealing with a series of result values, the from function can be used to combine everything together.

import{from}from"okej";constresults=[ok(1),ok("yes"),ok({data: "exists"})];// a Ok value will be returned if all input values are also Okconstresult=from(results);console.log(result.ok);// trueconsole.log(result.err);// trueconsole.log(result.data);// [ 1 , "yes" , { data: "exists" } ]

However if there are any values that are an instance of Err then the from function will return an Err response.

import{from}from"okej";constresults=[ok(1),err("noooo"),err("xxx",100)];// a Ok value will be returned if all input values are also Okconstresult=from(results);console.log(result.ok);// falseconsole.log(result.err);// falseconsole.log(result.errContext.results);// [ ok(1), err("noooo"), err("xxx", 100) ]

More on JavaScript's error handling issues

As mentioned at the start of the README, error handling in JavaScript is not so great.

Why can't we use try/catch everywhere

Try/catch does the job in JavaScript to capture and deal with errors, but the actual integrity of Error objects is a complete mess in the language.

Let's look at some custom form validation:

functionvalidateForm(formData: {[key: string]: unknown}): boolean{if(!formData.username){thrownewError("you didn't enter a username");}if(!formData.email){thrownewError("you didn't enter an email");}// yes it looks goodreturntrue;}

Now when the validateForm data is called, how does that look?

constmyFormData={ ... };// collected from some formletisValid=false;try{isValid=validateForm(myFormData);}catch(e){// `e` could be an error or even a string. How do we figure out what the error actually is?if(einstanceofError){if(/username/i.test(e.message)){// show the error in the form for the username// what if there are various errors here?}elseif(/email/i.test(e.message)){// show the error in the form for the email}else{// what now?}}else{// oh crap... what now?}}if(isValid){// finally.}else{// wait there's another error case here too?}

As made clear in the example code above, how does one deal with the error programmatically? Well using a Result object we can clean this up quite a lot.

import{ok,err,Result}from"okej";interfaceFormError{EmptyUsername,BadUsername,EmptyEmail,BadEmail,}functionvalidateForm(formData: {[key: string]: unknown): Result{if(!formData.username){returnerr("you didn't enter a username",FormError.EmptyUsername);}if(!formData.email){returnerr("you didn't enter an email",FormError.EmptyEmail);}returnok();}

And dealing with this error is just a matter of looking at the result response

constmyFormData={ ... };// collected from some formconstvalidationResponse=validateForm(myFormData);if(validationResponse.ok){// cool it works}else{switch(validationResponse.errCode){caseFormError.EmptyUsername:
// deal with the missing usernamebreak;caseFormError.EmptyEmail:
// deal with the missing emailbreak;default:
// catch allbreak;}}

Why does this library exist?

The ts-results library is the only other library out there in JS land that does something similar. This library does the job, however it closely follow's Rust's Result implementation which means that it brings in a lot of extra features that might not be necessary (i.e. Option values, Some/None types, Rx and mapping support as well as unwrapping). These features are essential in Rust, but they do introduce complexity and lead JS code into a direction that isn't common.

This library is just a small utility library that aims to stay lean and below the 2kb mark.

There is also the NPM library called results, but the README says to consider other tools.

It works with HTTP

Another good reason for this library is because it works with HTTP requests out of the box since no special function/object code is added around result objects.

// somewhere in the backendimportexpressfrom"express";import{err,ok}from"okej";constapp=express();app.get("/users",(req,res)=>{res.send(ok([{username: "user123"},{username: "user456"}]));});app.post("/users",(req,res)=>{// Ok or Err depending on if the user was createdconstresult=createUser(req.body.user);res.send(result);});

And the front-end can just consume the Ok and ErrResult values.

About

a bare-metal Result object implementation for TypeScript/JavaScript

Resources

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages