Lesson 15: Dev Tools - strvcom/frontend-academy-2022 GitHub Wiki

Speaker: Michal Honc

Resources


Error handling

Error handling is essential part of great UX. Current apps are really complex and it is almost impossible to prevent all errors from happening. But what is possible is to be ready for times when any error happen.

How are errors handle right now?

In the current state of our app we are not handling global errors. Any error that will happen and is not handled by the application logic will result in an app crash. Not a good solution if you ask me.

client-side error in production

How do errors differ on the server and on the client?

There is also a difference between a client side error and a server side error that can happen within getServerSideProps hook.

Server side error

500.tsx page

Server-rendering an error page for every visit adds complexity to responding to errors. To help users get responses to errors as fast as possible, Next.js provides a static 500 page by default without having to add any additional files.

We can refactor the NotFoundPage we are using for 404 and port it to a server side error handler.

500 Server Error page

const ErrorPage: NextPage<Props> = ({ title, desc, redirectTo, linkLabel }) => {
  return (
    <LayoutExternal>
      <HeadImage />
      <div>
        <Container>
          <Title>{title}</Title>
          <Description>{desc}</Description>

          <Link href={redirectTo} passHref>
            <Button as="a">{linkLabel}</Button>
          </Link>
        </Container>
      </div>
    </LayoutExternal>
  )
}

const ServerErrorPage = () => (
  <ErrorPage
    title="Something went wrong."
    desc={`Seems like Darth Vader just hits our website and drops it down.\n Please press the refresh button and everything should be fine again.`}
    redirectTo="" // empty to reload
    linkLabel="Refresh"
  />
)

Error boundary

Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed. Error boundaries catch errors during rendering, in lifecycle methods, and in constructors of the whole tree below them.

We can select which parts of our app will have error boundary and which fallback will be shown in the case of an error

// ErrorBoundary.tsx
import * as Sentry from '@sentry/nextjs'
import type { ReactNode } from 'react'
import { Component } from 'react'

import { ServerErrorPage } from '~/features/ui/components/ErrorPage'

type Props = {
  fallback?: ReactNode
  children: ReactNode
  type?: string
}

type State = {
  hasError: boolean
}

class ErrorBoundary extends Component<Props, State> {
  state = { hasError: false }

  static getDerivedStateFromError() {
    return { hasError: true }
  }

  componentDidCatch(error: Error) {
    const { type } = this.props

    Sentry.captureMessage(
      `Error catched in ErrorBoundary with type: ${type}`,
      'error'
    )
    Sentry.captureException(error)
  }

  render() {
    const { type, fallback, children } = this.props
    const { hasError } = this.state

    if (hasError) {
      if (type === 'root') {
        return <div>root error</div>
      }

      return fallback ?? <ServerErrorPage />
    }

    return children
  }
}

export { ErrorBoundary }

Usage

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ErrorBoundary type="next_root">
      <GlobalStyle />
      <HeadDefault />
      <QueryClientProvider client={queryClient}>
        <ErrorBoundary type="app_root">
          <UserContextProvider>
            <EventFilterContextProvider>
              <EventViewContextProvider>
                <Component {...pageProps} />
              </EventViewContextProvider>
            </EventFilterContextProvider>
          </UserContextProvider>
        </ErrorBoundary>
        <ReactQueryDevtools />
      </QueryClientProvider>
    </ErrorBoundary>
  )
}

Sentry

How to know whether app is working as it should for the users in production? For that matter we can use Sentry as an error tracking tool. It will catch all errors in your app and parse it into very informative report for developer. Stack trace, browser, browser version, OS, number of occurrences, and tons of other useful info is available for each error group.

How to start

  • Initialize Sentry yarn add @sentry/nextjs
  • Run sentry wizard npx @sentry/wizard -i nextjs
  • Add custom logging to our error boundary
// ErrorBoundary
componentDidCatch(error: Error) {
   const { type } = this.props
   Sentry.captureMessage(
      `Error catched in ErrorBoundary with type: ${type}`,
      'error'
   )
   Sentry.captureException(error)
}

Integrations

Sentry allows you to add many integrations with 3rd party services like Slack, Github, Vercel, Heroku, Netlify, etc. This can be useful for alerting new or recurring errors in Sentry or linking errors to Github issues.

Devtools

Console

Console is running in the same runtime as your app. Be sure to leverage that while developing or debuggin your app. Don't forget to remove console.log call in production to optimize the speed of your app. Leverage live expressions with live values while developing with Browser APIs

ReactJS devtools

To easily debug React application we can use ReactJS devtools extension into Chrome or Firefox. With Components tab we can inspect the whole ReactJS tree with all props, state, hooks and context. Profiler on the other hand helps with understanding how our ReactJS code runs, why it re-renders, and how fast it is rendering.

Network tab

The network tab within Devtools is your best friend while debugging anything related to app<->internet communication. That can be downloading images, fonts, scripts or fetching data from an API.

It will show you the order, size, HTTP status, initiator and many more useful information. You can use network speed throttling to test your app on slower networks like 3G.

Lighthouse

To get performance metrics of the whole app regardless of the framework used we can leverage Lighthouse. That is helping us measure core Web Vitals - https://web.dev/vitals/.

Debugging techniques

Divide and conquer

To understand complex logic it helps to split the problem into smaller chunks and understanding it step-by-step to built the whole picture. During debugging we can for example divide logic into functions that we can test individually and better pin point the root of an issue

Explain the bug to someone

Try to explain the bug to someone out loud. You can talk to your better half, your non-dev friend, mother, grandma, whoever.. it just has to said out loud.

It has a name in dev community as rubber duck debugging where you can essentially use a rubber duck to debug your code :)

image

Take a walk/shower

Do a different activity that will shift your mind into different mode and let the brain do its magic. From time to time the brain itself will unwind the issue and find a solution for it

⚠️ **GitHub.com Fallback** ⚠️