Skip to content
This repository has been archived by the owner on Jan 13, 2023. It is now read-only.

Global error handling use MST #339

Open
etovladislav opened this issue May 16, 2020 · 5 comments
Open

Global error handling use MST #339

etovladislav opened this issue May 16, 2020 · 5 comments

Comments

@etovladislav
Copy link

Hello everyone!

I’ve been looking for a solution for a very long time, I can’t find it anywhere.
How to make a global error handler from the api server response to show the user a modal window use MST or something else? Or for example, if the token has expired redirect to the authorization window, if the answer 500 came to show the Houston we have problems screen. depending on server response.

a single handler that will consider different cases and show info windows

🙏🏻

@rdewolff
Copy link
Contributor

Very interested in this topic as well. Maybe we could start by sharing our way to do it or our ideas? That would open up the discussion.

@jksaunders
Copy link
Contributor

jksaunders commented May 24, 2020

Hi! I don't have a repo example, but this is the rough code for what I've done for this in another project, in ignite-bowser's file structure:

dialog-store.ts

export const DialogStoreModel = types
  .model("DialogStore")
  .props({
    dialog,
  })
  .extend(withEnvironment)
  .views(self => ({}))
  .actions(self => ({
    setDialog: flow(function * (dialog) {
      self.dialog = dialog
    }),
 })
  .actions(self => ({
    initializeStore: flow(function * () {
      self.environment.setDialog = dialog => self.setDialog(dialog)
    }),
  }))

in setup-root-store.ts

Object.keys(rootStore).forEach(key => {
    if (rootStore[key] && typeof rootStore[key].initializeStore === 'function') {
      rootStore[key].initializeStore()
    }
  })

environment.ts

export class Environment {
  constructor() {
    this.api = new Api({
      // this is a made up constructor! Whatever you use, you can set up your tooling here to do this on error
      onError: (error) => {
        if (this.onError) {
          this.setDialog(errorToDialog(error))
        }
      }
    })

  api: Api

  setDialog: (dialog: any) => void
}

app-wrapper.ts

export const AppWrapper: React.FunctionComponent<AppWrapperScreenProps> = observer(props => {
  const nextScreen = React.useMemo(() => () => props.navigation.navigate("demo"), [
    props.navigation,
  ])

  const { dialogStore } = useStores()

  return (
    <View>
      <SomeContent />
      {dialogStore.dialog}
    </View>
  )
}

The setup works like this:

  • the environment is created in setup-root-store.ts
  • it has a null (for now) onError function
  • the API is initialized with all requests calling onError if there is an error
  • when the dialogStore is created using withEnvironment, call initializeStore during setup-root-store so that environment's onError is hooked up to dialogStore
  • use the dialogStore on some app wrapper component that always shows the dialogStore's dialog if it has one

That way, the app wrapper is observing the dialogStore's dialog, and all requests made by the API library in environment are hooked up to dialogStore.

I'm sure someone who's more MST-savvy can make this even cleaner somehow!

Example repo incoming 👀

Edit: example repo at https://github.com/jksaunders/ignite-bowser-339

^ In this repo, the mobile app has a valid API request button (no errors shown) and an invalid API request button (error message briefly shown). It's all from a single handler, observable anywhere!

@viralS-tuanlv
Copy link

for your ex, catch the token expires, please check api-problem.ts, and here how can i handle the token expires:
`export function getErrorMessage(data: any): string {
try {
if (data.message != null) {
return data.message
} else {
return translate('errors.serverError')
}
} catch (e) {
return translate('errors.serverError')
}
}
const doExpire = async (msg: string) => {
storage.remove(common.TOKEN)
rootStore.toast.show(msg)
await delay(500)
rootStore.auth.setAuth(false)
RootNavigation.resetRoot({ routes: [] })
rootStore.resetAll()
}
export function getGeneralApiProblem(response: ApiResponse): GeneralApiProblem | void {
switch (response.problem) {
case "CONNECTION_ERROR":
return { kind: "cannot-connect", temporary: true }
case "NETWORK_ERROR":
return { kind: "cannot-connect", temporary: true }
case "TIMEOUT_ERROR":
return { kind: "timeout", temporary: true }
case "SERVER_ERROR":
return { kind: "server", data: getErrorMessage(response.data) }
case "UNKNOWN_ERROR":
return { kind: "unknown", temporary: true }
case "CLIENT_ERROR":
switch (response.status) {
case 401:
doExpire(getErrorMessage(response.data))
return { kind: "unauthorized", data: getErrorMessage(response.data) }
case 403:
return { kind: "forbidden", data: getErrorMessage(response.data) }
case 404:
return { kind: "not-found", data: getErrorMessage(response.data) }
default:
return { kind: "rejected", data: getErrorMessage(response.data) }
}
case "CANCEL_ERROR":
return null
}

return null
}
`

@etovladislav
Copy link
Author

Hi! I don't have a repo example, but this is the rough code for what I've done for this in another project, in ignite-bowser's file structure:

dialog-store.ts

export const DialogStoreModel = types
  .model("DialogStore")
  .props({
    dialog,
  })
  .extend(withEnvironment)
  .views(self => ({}))
  .actions(self => ({
    setDialog: flow(function * (dialog) {
      self.dialog = dialog
    }),
 })
  .actions(self => ({
    initializeStore: flow(function * () {
      self.environment.setDialog = dialog => self.setDialog(dialog)
    }),
  }))

in setup-root-store.ts

Object.keys(rootStore).forEach(key => {
    if (rootStore[key] && typeof rootStore[key].initializeStore === 'function') {
      rootStore[key].initializeStore()
    }
  })

environment.ts

export class Environment {
  constructor() {
    this.api = new Api({
      // this is a made up constructor! Whatever you use, you can set up your tooling here to do this on error
      onError: (error) => {
        if (this.onError) {
          this.setDialog(errorToDialog(error))
        }
      }
    })

  api: Api

  setDialog: (dialog: any) => void
}

app-wrapper.ts

export const AppWrapper: React.FunctionComponent<AppWrapperScreenProps> = observer(props => {
  const nextScreen = React.useMemo(() => () => props.navigation.navigate("demo"), [
    props.navigation,
  ])

  const { dialogStore } = useStores()

  return (
    <View>
      <SomeContent />
      {dialogStore.dialog}
    </View>
  )
}

The setup works like this:

  • the environment is created in setup-root-store.ts
  • it has a null (for now) onError function
  • the API is initialized with all requests calling onError if there is an error
  • when the dialogStore is created using withEnvironment, call initializeStore during setup-root-store so that environment's onError is hooked up to dialogStore
  • use the dialogStore on some app wrapper component that always shows the dialogStore's dialog if it has one

That way, the app wrapper is observing the dialogStore's dialog, and all requests made by the API library in environment are hooked up to dialogStore.

I'm sure someone who's more MST-savvy can make this even cleaner somehow!

Example repo incoming 👀

Edit: example repo at https://github.com/jksaunders/ignite-bowser-339

^ In this repo, the mobile app has a valid API request button (no errors shown) and an invalid API request button (error message briefly shown). It's all from a single handler, observable anywhere!

Thank you so much! It really helped

@rodgomesc
Copy link

rodgomesc commented Dec 7, 2021

thanks for sharing \o/, another suggestion here instead of changing the logic to use axiosInstance just to catch the onError, we can simply add a monitor to sauceApi, and filter by response.problem,

import * as SecureStorage from "@utils/secure-storage"

import { Api } from "../services/api"
import { ACCESS_TOKEN_KEY } from "@utils/constants"

/**
 * The environment is a place where services and shared dependencies between
 * models live.  They are made available to every model via dependency injection.
 */
export class Environment {
  constructor() {
    this.api = new Api()
  }

  async setup() {
    await this.api.setup()
   
    this.api.apisauce.addMonitor((response) => {
      if (this.setResponse) {
        this.setResponse(response)
      }
    })
  }

  /**
   * Our api.
   */
  api: Api

  setResponse: (response: any) => void
}

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants