Electron Adventures: Episode 52: Displaying Error Messages
Tags: javascript, electron, svelte
Published: 2021-09-14 07:10:45

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`:

```javascript
  $: 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, "..")
      }
    }
  }
```

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.

```html
<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>
```

### `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.

```javascript
function openPalette() {
  dialog = {type: "CommandPalette"}
}
function openDialog(type, data) {
  dialog = {type, data}
}
```

### `src/Panel.svelte`

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

```javascript
  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})
  }
```

### `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.

```javascript
  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()
    }
  }
```

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

```css
  form {
    padding: 8px;
    background: #338;
    box-shadow: 0px 0px 24px #004;
  }
```

### `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.

```javascript
  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()
  }
```

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.

```html
<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>
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bqs6v1y5feq4rc73vwt6.png)

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](https://github.com/taw/electron-adventures/tree/master/episode-52-displaying-error-messages).