Skip to content

Handling Errors in Solid 2.0

Posted on:May 25, 2026

This article targets the current Solid 2.0 beta APIs as of beta.14. Some APIs may evolve before stable release.

So after my last article I started playing around with the code a bit more and decided to see how difficult it would be to add errors and retries. This is the first question a lot of people ask after seeing the simple action API.

const [optimisticComments, setOptimisticComments] = createOptimisticStore(
  () => getComments(props.issue.id),
  []
);

const addCommentAction = action(function* (comment: string) {
  const newComment = {
    id: crypto.randomUUID(),
    createdAt: new Date().toISOString(),
    author: "Brenley Dueck",
    body: comment,
  };
  setOptimisticComments(comments => {
    comments.push(newComment);
  });
  yield props.saveCommentAction(props.issue.id, newComment);
  refresh(optimisticComments);
});

One subtle but important detail here is that actions are generators rather than async functions. Yielding lets Solid coordinate optimistic updates, refreshes, and transitions as a single async workflow.

Let’s first simulate some errors in api.ts

 export const addComment = async (issueId: number, comment: Comment) => {
   await new Promise(resolve => setTimeout(resolve, 2000));
+  if (Math.random() < 0.5) {
+     console.log("Failed to add comment");
+     throw new Error("Failed to add comment");
+  }
   commentsByIssueId[issueId].push({ ...comment, time: "Just now" });
   return commentsByIssueId[issueId];
 };

Now if we try to add a comment, it will fail about 50% of the time.

Right now the comment is added optimistically as expected, but after a couple seconds it disappears because the refreshed server state doesn’t contain it.

Let’s keep it around on a failure. We can keep track of erroredComments in a separate array.

export function DetailPanel(props: { issue: Issue, saveCommentAction: (issueId: number, comment: Comment) => Promise<unknown> }) {
+ const erroredComments: any[] = [];

  const [optimisticComments, setOptimisticComments] = createOptimisticStore(() => getComments(props.issue.id), []);

  const addCommentAction = action(function* (comment: string) {
    const newComment = {
      id: crypto.randomUUID(),
      createdAt: new Date().toISOString(),
      author: "Brenley Dueck",
      body: comment
    }
    setOptimisticComments(comments => {
      comments.push(newComment);
    });
+    try {
      yield props.saveCommentAction(props.issue.id, newComment);
+   } catch (_error) {
+      erroredComments.push(newComment);
+   }
    refresh(optimisticComments);
  });

Now that we have this how do we integrate this into our actual state? Turns out it’s pretty easy. We add it to our createOptimistic store as an overlay.

const [optimisticComments, setOptimisticComments] = createOptimisticStore(
  async () => {
    const comments = await getComments(props.issue.id);
    return comments.concat(erroredComments)
  }
  []
);

Now if it errors it will remain in optimisticComments.

One thing we forgot is to add a way for the UI to tell which are errored.

    try {
      yield props.saveCommentAction(props.issue.id, newComment);
   } catch (_error) {
+      erroredComments.push({...newComment, errored: true});
   }

Then in our CommentCard we can do something like:

<span>{props.comment.time ?? (props.comment.errored && "Failed to save")}</span>

This is a pretty naive implementation so far. We can make an improvement.

Because we have a timestamp of when a comment was added we can sort by this to make sure the comments stay in the right order. Otherwise the errored ones will always move to the bottom of the list.

 const [optimisticComments, setOptimisticComments] = createOptimisticStore(
    async () => {
      const comments = await getComments(props.issue.id);
      return comments.concat(erroredComments)
+          .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
    },
    []
  );

Now let’s add a retry button to our JSX and a retryComment action. Because we have an id we know which erroredComment to remove.

<span>
  {props.comment.time ??
    (props.comment.errored ? (
      <button
        onClick={() => {
          props.retryComment(props.comment);
        }}
      >
        Retry
      </button>
    ) : (
      "Saving..."
    ))}
</span>
const retryComment = action(function* (comment: Comment) {
  try {
    yield props.saveCommentAction(props.issue.id, comment);
    erroredComments.splice(
      erroredComments.findIndex(c => c.id === comment.id),
      1
    );
  } catch (_error) {
    // errored again so leave it in the list
  }
  refresh(optimisticComments);
});

So this is mostly working the way we want. Finally, let’s add optimistic UI for retries.

const [retrying, setRetrying] = createOptimistic(false);

<span>
  {props.comment.time ??
    (props.comment.errored ? (
      <button
        onClick={() => {
          setRetrying(true);
          props.retryComment(props.comment);
        }}
      >
        {retrying() ? "Retrying..." : "Retry"}
      </button>
    ) : (
      "Saving..."
    ))}
</span>;

The nice thing about this approach is that errors and retries don’t require abandoning the optimistic flow. We can progressively layer failed state, retry behavior, and local UI state on top of the same primitives.