Errors

Summary

This page describes how we handle errors in Dendron.

Details

  • If a function can return multiple errors, use DendronCompositeError to wrap up multiple errors. If you need to look into the errors inside a DendronCompositeError, use errorsList() to grab the errors inside the composite error.
  • When returning errors from a server, use error2PlainObject to extract the common properties
  • When logging errors, use stringifyError (regular stringify will omit fields)
  • If an error is non-fatal, meaning the function was able to complete despite the error (or that the error is a warning), then set the error severity to ERROR_SEVERITY.MINOR. Not all but some code like engine initialization will recognize this and consider the operation successful.

If you need additional well-typed information with an error, or you're trying to handle a specific type of error, you can do so by extending the DendronError class. For an example see errorTypes.ts and ReloadIndex.

RespV3

Note: This is depricated! Use Result

Use this when working with functions that return data or an error

type RespV3 = {
      error: IDendronError;
      data?: never;
    }
  | {
      error?: never;
      data: T;
    };

This type signature says that that the result can either contain an error property or a data property but never both at the same time. This is useful when an error shortcircuits the calling function.

  • NOTE: typescript isn't very smart about destructuring. for type narrowing to work, you can't destructure the argument
    • bad
    const {error, data} = someFunc
    if (error) {
      throw 
    }
    // COMPILE ERROR
    data.value
    
    • good
    const resp = someFunc
    if (resp.error) {
      throw 
    }
    resp.data.value
    

Example


// doFoo returns either an error or the data
function doFoo(): RespV3 {
  ...
  if (error) {
    return {error: new DendronError(...)}
  }

  return {
    data: ....
  }
}


function main() {

  const resp = doFoo();
  if (resp.error) {
    // handle error...
  }

  // if error hasn't happened, we know `data` exists and is valid
  doBar(resp.data)
}

Result (using neverthrow package)

The Result is an alternative to RespV3 and should be considered as the new way to handle errors. They are similar in the way that both provide an failure and success track but are different in Result having properties that allows for more precision and accuracy while also improve the ergomonics when working with errors. Main differences are that Result:

  • is a discriminated union type, therefor allows for type narrowing.
  • treats errors even more as a first-class citizen by which errors are not treated as an exception but rather a form of data/information like any other data. This is done using a two-tack system
  • provides for utils to easily wrap/unrwap, map and capture/ensure/safeguard.
  • is thenable meaning it behaves exactly like a native Promise<Result> (ResultAsync in this case)

For a better grasp of the concept read https://github.com/supermacro/neverthrow/wiki/Introduction:-Type-Safe-Errors-in-JS-&-TypeScript-(10-minute-read)

Examples

Basic

Synchronous API

Result is defined as follows:

type Result<T, E> = Ok<T, E> | Err<T, E>

Ok<T, E>: contains the success value of type T

Err<T, E>: contains the failure value of type E

import { ok, err } from "@dendronhq/common-all"

// something awesome happend

const yesss = ok(someAesomeValue)

// moments later ...

const mappedYes = yesss.map(doingSuperUsefulStuff)

if (mappedYes.isOk()) {
  doStuffWith(mappedYes.value)
} else {
  doStuffWith(mappedYes.error)
}
Asynchronous API

Asynchronous methods can return a ResultAsync type instead of a Promise<Result> in order to enable further chaining.

ResultAsync is thenable meaning it behaves exactly like a native Promise<Result>: the underlying Result can be accessed using the await or .then() operators.

This is useful for handling multiple asynchronous apis like database queries, timers, http requests, ...


import { ResultAsync, IDendronError } from "@dendronhq/common-all"

// lets create a synchronous method that returns a `ResultAsync
function createNote(note: Note): ResultAsync<Note, IDendronError> {
	return ResultAsync.fromPromise(createNote(note), () => new Error('Note creation error'))
}

// We can now call the method above
const asyncResult = createNote({ id: '123', body: 'foo'}) // asyncRes is a `ResultAsync<Note, Error>`

// We can chain the ResultAsync to build another ResultAsync
const asyncResult2 = asyncRes.map((note: Note) => note.body) // asyncRes2 is a `ResultAsync<string, Error>`

// A ResultAsync acts exactly like a Promise<Result>
// It can be transformed back into a Result just like a Promise would:

// using await
const res = await asyncResult
// res is a Result<string, Error>
if (res.isErr()) {
  console.log('Oops fail: ' + res.error.message)
} else {
  console.log('Successfully created note ' + res.value)
}

// using then
asyncResult2.then(res => {
  // res is Result<string, Error>
  if (res.isErr()) {
    console.log('Oops fail: ' + res.error.message)
  } else {
    console.log('Successfully created note ' + res.value)
  }
})

Accessing the value inside a Result

import { ok, err } from "@dendronhq/common-all"

const example1 = ok(123)
const example2 = err('abc')

// neverthrow uses type-guards to differentiate between Ok and Err instances
// Mode info: https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types
if (example1.isOk()) {
  // using type guards, we can access an Ok instance's `value` field
  doSomethingUseful(example1.value)
} else {
  // because of type guards
  // typescript knows that example1 is an Err instance and thus has a `error` field
  handleError(example1.error)
}

if (example2.isErr()) {
  // you now have access to example2.error
  handleError(example2.error)
} else {
  // you now have access to example2.value
  doSomethingUseful(example2.value)
}

Wrap 3rd party code to localize exceptions

The JavaScript community has agreed on the convention of throwing exceptions. As such, when interfacing with third party libraries it's imperative that you wrap third-party code in try / catch blocks.

import { Result, ResultAsync } from 'neverthrow'

// Synchronous
const safeJsonParse = Result.fromThrowable(
  JSON.parse, 
  (error: unknown) => intoError(error)
)
// `safeJsonParse` has a type of Result<Json, Error>

// Async
const getById = (id: string) => ResultAsync.fromPromise(
  fetch(`/some_url/${id}`),
  (error: unknown) => intoError(error)
)
// `getById` has a type of ResultAsync<Data, Error>

Creating more intuitive/context-aware types

import { Result, IDendronError } from "@dendronhq/common-all"

type MyContextResult<T> = Result<T, IDendronError>;

References

Sentry

We use Sentry to monitor the code for exceptions. You can use Sentry by wrapping a function using sentryReportingCallback. For example:

export const provideCompletionItems = sentryReportingCallback(
  (document: TextDocument, position: Position) => {
    // ...
  }
);

One issue here: the sentry wrapper cause the callback function to lose its this value. If you are passing a method to this function, you must bind the this value:

class Foo {
  private callback() { /* ... */ }

  public setupCallback() {
    const wrappedCallback = sentryReportingCallback(
      this.callback.bind(this)
    );
    // ...
  }
}

Otherwise, when the callback function is called the this value will be undefined.

API

ERROR_STATUS

These match to common errors in Dendron. You can find the full list here

ERROR_SEVERITY

/**
 * Labels whether error is recoverable or not
 */
export enum ERROR_SEVERITY {
  /**
   * Recoverable 
   */
  MINOR = "minor",
  /**
   * Non-recoverable 
   */
  FATAL = "fatal",
}

Changelog

Past Discussions


Children
  1. Error Reporting

Backlinks