🛠️ Todolist: Handling Optimistic UI errors

Hey ,

I'm thrilled to help you learn JavaScript. Unfortunately, you've landed on a page where you cannot access with your current purchase.

Please upgrade (use this link) access this content.

I'm super eager to help you learn more!

🛠️ Todolist: Handling Optimistic UI errors

Requests don’t fail much. If they fail, they fail because of these two reasons:

  1. Connection errors
  2. Servers returning errors

Connection errors

You won’t be able to send requests or receive responses when you’re offline.

Try sending a request without internet connection. You’ll see an error like this (assuming you’ve downloaded zlFetch onto your computer and linked it to your index.html file).

<body>
  <!-- Linked zlFetch locally -->
  <script src="js/zl-fetch.js"></script>
  <script src="js/main.js"></script>
</body>
zlFetch('https://api.learnjavascript.today/users/transporterduo')
  .then(response => { console.log(response) })
  .catch(error => console.error(error))
Fetch error message without internet connection

You also won’t be able to receive a response from a request if your internet connection gets cut off midway.

One way to simulate a cut-off in connection is to disconnect your Wifi while sending a request.

// 10.255.255.1 gives you time to disconnect your wifi
zlFetch('http://10.255.255.1')
  .then(response => { console.log(response) })
  .catch(error => console.error(error))

You’ll get an error message that looks like this:

Fetch error message when the internet connection cuts off.

In both cases, you’ll get TypeError: Failed to fetch as your error message. You won’t be able to get the net:: part in JavaScript.

Handling connection errors

Some apps handle connection errors by telling users they’re offline. Slack does this, for example.

Slack's warning message when the user is offline.

We can copy Slack and tell users they’re offline too.

First, we’ll use navigator.onLine to check whether a user is online. If the user is online, navigator.onLine return true. If the user is offline, navigator.onLine returns false

console.log(navigator.onLine) // true or false, depending on whether you're online or offline

Second, we’ll adjust the error message depending on whether the user is online.

todolist.addEventListener('submit', ev => {
  // ...

  zlFetch(/*...*/)
    .then(/*...*/)
    .catch(_ => {
      // ...

      const errorMessage = navigator.onLine
        ? 'Cannot add task. Please try again later.'
        : `It seems like you're offline. Please go online to use the Todolist.`
      // ...
    })
})

If the user goes offline while sending a request, they’ll see this message:

Offline error message

If they’re online, they’ll see this message:

Online error message

A better way to handle connection errors

We don’t have to wait for a user to send a request before we check if they’re offline. We can tell whether they’re online or offline anytime with two events.

  1. online: Fires when user comes online
  2. offline: Fires when user goes offline

We can build a warning message to inform users when the moment they go offline. It’s friendly compared to an error message.

Offline warning message.

We can build the warning message with the following HTML:

<div class="flash-container">
  <div class="flash flash--connection" data-type="warning">
    <svg class="flash__icon"> <!-- ... --> </svg>
    <span class="flash__message">It seems like you're offline. Please go online to use the Todolist.</span>
    <button class="flash__close"> <!-- ... --> </button>
  </div>
</div>

We’ll only show the message when the user is offline. To do this, we can add a custom attribute called data-connection-status to the <body> element.

navigator.onLine
  ? document.body.dataset.connectionStatus = 'online'
  : document.body.dataset.connectionStatus = 'offline'

And we can use the following CSS to show or hide the warning message.

.flash--connection {
  visibility: hidden;
}

[data-connection-status="offline"] .flash--connection {
  visibility: visible;
}

If a user goes offline when they’re using the Todolist, they’ll trigger an offline event. Likewise, if they come online when they’re using the Todolist, they’ll trigger an online event.

We’ll use the events to update the connection status.

function setConnectionStatus () {
  navigator.onLine
    ? document.body.dataset.connectionStatus = 'online'
    : document.body.dataset.connectionStatus = 'offline'
}

setConnectionStatus()
window.addEventListener('online', setConnectionStatus)
window.addEventListener('offline', setConnectionStatus)
Toggling between online and offline states.

Since we have a message that warns the user when they’re offline, we can remove connection errors from the error handler.

todolist.addEventListener('submit', event => {
  // ...

  zlFetch(/*...*/)
    .then(/*...*/)
    .catch(_ => {
      // ...
      const errorMessage = 'Cannot add task. Please try again later.'
      // ...
    })
})

If you go offline and refresh the Todolist, you’ll see a list of error messages. You get these errors because the browser can’t fetch libraries when you’re offline. (You can’t send requests or receive responses. Remember?)

Error requesting libraries

You can mitigate these errors by downloading the libraries into your project. (Highly recommended for real projects).

Anyway, the zlFetch is not defined error prevents the rest of the JavaScript file from executing. If you want online/offline code to work, you’ll want to put them at the start of the JavaScript file.

// Put these at the start of your JavaScript file
setConnectionStatus()
window.addEventListener('online', setConnectionStatus)
window.addEventListener('offline', setConnectionStatus)

Server errors

Servers return errors because of three reasons:

  1. The browser can’t reach the server
  2. Something went wrong in the server
  3. Client errors

When the browser can’t reach the server

It’s a connection error when the browser can’t reach the server. You’ll also get an error message that says TypeError: Failed to fetch. This happens even if the server goes offline.

Fetch error when it fails to reach server.

In this case, we don’t know why we can’t reach the server. The best error message we can say is Failed to reach server. It’s more friendly compared to TypeError: Failed to fetch.

todolist.addEventListener('submit', event => {
  // ...

  zlFetch(/*...*/)
    .then(/*...*/)
    .catch(error => {
      // ...
      let errorMessage = ''
      const { message } = error.body
      if (message = "TypeError: Failed to fetch") {
        errorMessage = 'Failed to reach server. Please try again later.'
      } else {
        errorMessage = 'Cannot add task. Please try again later.'
      }
      // ...
    })
})

I usually to add a Please try again later message just incase something went wrong with the connection temporarily. If the user gets the Please try again later message in two different occasions, they’ll usually contact me so I can fix the problem.

Something went wrong with the server

Developers who write code for servers can make mistakes too. If something goes wrong inside the server (that has nothing to do with your request), the server should return a 500+ Internal Server Error.

We don’t know what error messages might come from the server in this case. It might be something weird like E11000 duplicated key error if the developer did not send a proper error message.

What we can do is use the error message from the server as a default. If we notice a specific error message later, we can replace it with our own (like the TypeError one).

todolist.addEventListener('submit', event => {
  // ...

  zlFetch(/*...*/)
    .then(/*...*/)
    .catch(error => {
      // ...
      if (message = "TypeError: Failed to fetch") {
        errorMessage = 'Failed to reach server. Please try again later.'
      } else {
        errorMessage = error.body.message
      }
      // ...
    })
})

Client errors

Client errors are 400+ errors. Examples include:

  1. 400 Bad request
  2. 401 Unauthorized
  3. 403 Forbidden
  4. 404 Not found

Again, we don’t know what error messages come from the backend. We can only hope the developer created proper error messages for us.

If you get a friendly error message, you can use the error message directly. For example, if you send in a Bad Request, you’ll get a friendly error message that says Task requires a name.

Response returns with an error.

Sometimes, you don’t get friendly messages. For example, if you sent set a wrong username or password for the Authorization header, you’ll get an error message that says Unauthorized.

Unauthorized error.

Unauthorized isn’t friendly. We can choose to change it up to be slightly more friendly.

todolist.addEventListener('submit', event => {
  // ...

  zlFetch(/*...*/)
    .then(/*...*/)
    .catch(error => {
      // ...
      if (message = 'TypeError: Failed to fetch') {
        errorMessage = 'Failed to reach server. Please try again later.'
      } else if (message = 'Unauthorized') {
        errorMessage = 'Invalid username or password. Please check your username or password.'
      } else {
        errorMessage = error.body.message
      }
      // ...
    })
})

That’s it!