DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

Electron Adventures: Episode 52: Displaying Error Messages

As we manage files there will be errors, and we need to handle them somehow.

Panel unable to fetch directory listing

The first error condition we should deal with is when the panel is unable to fetch the directory listing. This could happen because user tries to enter directory belonging to another user. Or because the directory got deleted and panel is now trying to refresh.

There is a correct solution to this, and the correct solution is to do absolutely nothing. If user tries to navigate to directory they have no access to, just stay where they are. If directory is gone, just keep going up a level until file manager reaches directory that is accessible. This behaviour is much more understandable to the user than error popups, as it should be completely obvious to the user where they still are.

Even more optimally we could display some sort of feedback, as long as it didn't stop the user. Right now we don't have any such mechanism.

Here are relevant changes to src/Panel.svelte:

  $: fetchFiles(directory)

  async function fetchFiles() {
    try {
      files = await window.api.directoryContents(directory)
      setInitialSelected()
      setInitialFocus()
    } catch (err) {
      console.log(err)
      if (directory === "/") {
        files = []
      } else {
        initialFocus = path.basename(directory)
        directory = path.join(directory, "..")
      }
    }
  }
Enter fullscreen mode Exit fullscreen mode

Unfortunately current implementation resets selection on error. Keeping selection on failed navigation would require a bit more bookkeeping.

Errors we want to display

If creating new directories and deleting files fails, error should be displayed, as it's not obvious what is the fallback.

Taking a step back

As I was writing this, I noticed that the fancy dialog system I setup in the previous episode wasn't actually doing what I needed. So we'll have to go through a lot of files again and I'll try to explain what I had to change and why.

src/Dialog.svelte

The fancy metaprogramming I setup wasn't actually working very well when I tried to transition from one open dialog (mkdir or delete) directly to another open dialog (error). Svelte supports $$props for all props, but doesn't automatically react to new unknown props being added or removed from it while component is mounted, so we'd need to write a bit of extra code.

So instead I changed it to use two props - type and data. That's a bit extra verbosity upstream, but it would get a bit difficult to understand otherwise.

Also because error dialog needs to be of a different color, some of the styling got moved into individual dialogs.

<script>
  import CommandPalette from "./CommandPalette.svelte"
  import DeleteDialog from "./DeleteDialog.svelte"
  import MkdirDialog from "./MkdirDialog.svelte"
  import ErrorDialog from "./ErrorDialog.svelte"

  let component = {CommandPalette, MkdirDialog, DeleteDialog, ErrorDialog}

  export let type
  export let data = {}
</script>

<div>
  <svelte:component this={component[type]} {...data}/>
</div>

<style>
  div {
    position: fixed;
    left: 0;
    top: 0;
    right: 0;
    margin: auto;
    max-width: 50vw;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

src/App.svelte

Instead of having event handler per dialog, App component just has a single openDialog method.

The exception is openPalette which stayed separate, because this one goes directly from a keyboard shortcut, so we need some target that gets invoked without any arguments. It could be defined as openDialog("CommandPalette") too.

function openPalette() {
  dialog = {type: "CommandPalette"}
}
function openDialog(type, data) {
  dialog = {type, data}
}
Enter fullscreen mode Exit fullscreen mode

src/Panel.svelte

The F7 and F8 handlers changed to use the new API.

  function createDirectory() {
    app.openDialog("MkdirDialog", {base: directory})
  }
  function deleteFiles() {
    let filesTodo
    if (selected.length) {
      filesTodo = selected.map(idx => files[idx].name)
    } else if (focused && focused.name !== "..") {
      filesTodo = [focused.name]
    } else {
      return
    }
    app.openDialog("DeleteDialog", {base: directory, files: filesTodo})
  }
Enter fullscreen mode Exit fullscreen mode

src/MkdirDialog.svelte

We need to add a try/catch block. The catch section logs the error both to console and to the error dialog. We still need to call refresh even if an error happened.

  function submit() {
    app.closeDialog()
    if (dir !== "") {
      let target = path.join(base, dir)
      try {
        window.api.createDirectory(target)
      } catch (err) {
        console.log(`Error creating directory ${target}`, err)
        app.openDialog("ErrorDialog", {error: `Error creating directory ${target}: ${err.message}`})
      }
      bothPanels.refresh()
    }
  }
Enter fullscreen mode Exit fullscreen mode

Styling also got a section on how to color this dialog:

  form {
    padding: 8px;
    background: #338;
    box-shadow: 0px 0px 24px #004;
  }
Enter fullscreen mode Exit fullscreen mode

src/DeleteDialog.svelte

We need a try/catch block here as well. We actually need to do refresh and return in the loop in case of error, as normally we close the dialog once we finish, but if we just break from the loop we'd be closing error dialog e just opened.

Because this error comes from running external program to move things to trash, it's honestly quite terrible. I don't know if there's any better JavaScript packages for moving files to trash. If you know of any, let me know in the comments.

  async function submit() {
    for (let file of files) {
      let fullPath = path.join(base, file)
      try {
        await window.api.moveFileToTrash(fullPath)
      } catch(err) {
        console.log(`Error deleting file ${fullPath}`, err)
        app.openDialog("ErrorDialog", {error: `Error deleting file ${fullPath}: ${err.message}`})
        bothPanels.refresh()
        return
      }
    }
    app.closeDialog()
    bothPanels.refresh()
  }
Enter fullscreen mode Exit fullscreen mode

It also got the same styling as MkdirDialog.

src/ErrorDialog.svelte

ErrorDialog only has OK button, which is totally fine, as it's purely informational, and that OK does not represent any action. Using OK buttons to confirm an action is terrible design I complained about many times before, but that's not what we're doing here - we're just informing the user.

<script>
  export let error

  import { getContext } from "svelte"

  let { eventBus } = getContext("app")
  let app = eventBus.target("app")

  function submit() {
    app.closeDialog()
  }
  function focus(el) {
    el.focus()
  }
</script>

<form on:submit|preventDefault={submit}>
  <div>{error}</div>
  <div class="buttons">
    <button type="submit" use:focus>OK</button>
  </div>
</form>

<style>
  form {
    padding: 8px;
    background: #833;
    box-shadow: 0px 0px 24px #400;
  }

  .buttons {
    display: flex;
    flex-direction: row-reverse;
    margin-top: 8px;
    gap: 8px;
  }

  button {
    font-family: inherit;
    font-size: inherit;
    background-color: #b66;
    color: inherit;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

I feel like there's too much boilerplate here for something so simple, and maybe we should move some of those things out.

Also I don't love this shade of red.

Result

Here's the results:

Episode 52 Screenshot

In the next episode, we'll take a break from the file manager for a while, and see what other interesting things we can do in Electron. This series turned a bit too much into File Manager Development series, and while the file manager keeps bringing new interesting issues to talk about, that's not quite what I had in mind.

As usual, all the code for the episode is here.

Discussion (0)