Convex 1.3 brings a new callback-based JavaScript client for using Convex in non-React apps and a way to propagate error information for better error handling UX. We’ve also made a bunch of improvements to our CLI and runtime!

Callback-based JavaScript client

The reactive paradigm is a great fit for Convex subscriptions but not everyone uses React, the library. There’s a new callback-based Convex JavaScript client for subscribing to queries and running mutations and actions; anything you’d do with the ConvexReactClient but with 100% fewer hooks!

import { ConvexClient } from "convex/browser";
import { api } from "./convex/_generated/api.js";

const client = new ConvexClient(process.env["CONVEX_URL"]);

client.onUpdate(api.messages.list, {}, (messages) => {
  // do whatever you want with messages

This new client works the same way in browsers and Node.js.

Sometimes you just want your query results with no fuss: no installing packages, no build process, no TypeScript. A minimal HTML file watching a Convex query looks like this:

<!DOCTYPE html>
<script src=""></script>
  const client = new convex.ConvexClient("CONVEX_URL_GOES_HERE");
  client.onUpdate("messages:list", {}, (messages) => {
    // do whatever you want with messages

Use ConvexError to build error UIs

Before 1.3 all errors thrown from queries, mutations and actions looked the same to client applications talking to a production deployment: A plain Error with no additional information to discern what caused the error.

With the new version, you can now propagate data alongside thrown errors by throwing a ConvexError:

// convex/muFunctions.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";

export const createTask = mutation({
  args: { text: v.string() },
  handler: async (ctx, args) => {
    if (text.length === 0) {
      // This data will be accessible from the client
      throw new ConvexError({
        message: "Empty text not allowed"
        severity: "FATAL",
    const newTaskId = await ctx.db.insert("tasks", { text: args.text });
    return newTaskId;

You can propagate data from queries, mutations and actions called from other actions as well.

// src/App.tsx
import { ConvexError } from "convex/values";
import { useMutation } from "convex/react";
import { api } from "../convex/_generated/api";

export function MyApp() {
  const doSomething = useMutation(api.myFunctions.mutateSomething);
  const handleSomething = async () => {
    try {
      await doSomething();
    } catch (error) {
      const errorMessage =
        error instanceof ConvexError
          ? ( as { message: string }).message
          : "Unexpected error occurred";
      // ...
  // ...

Check out the docs for full details.

Server logs in the terminal

You can now run npx convex dev --tail-logs to see console.logs run inside queries, mutations, actions and even HTTP actions!

Other improvements

  • ConvexReactClient's query() method now rejects if the query function threw.
  • NPM libraries can now target the Convex default runtime specifically with the convex condition:
// package.json
  "exports": {
    ".": {
      "convex": "./forConvex.js"
      "default": "./index.js"
  • We’ve added ReadableStream, EventTarget and a bunch of other browser APIs to the Convex default runtime, you can now find the full list of Supported APIs in our docs.