{"record":{"id":"f6db69c19e757ef3","repo":"clockworklabs/SpacetimeDB","slug":"subscriptions-must-have-at-least-one-query","errorCode":null,"errorMessage":"Subscriptions must have at least one query","messagePattern":"Subscriptions must have at least one query","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"crates/bindings-typescript/src/sdk/subscription_builder_impl.ts","lineNumber":120,"sourceCode":"    ) => RowTypedQuery<any, any> | RowTypedQuery<any, any>[]\n  ): SubscriptionHandleImpl<RemoteModule>;\n  subscribe(\n    query_sql:\n      | string\n      | RowTypedQuery<any, any>\n      | Array<string | RowTypedQuery<any, any>>\n      | ((tables: any) => RowTypedQuery<any, any> | RowTypedQuery<any, any>[])\n  ): SubscriptionHandleImpl<RemoteModule> {\n    let queries: Array<string | RowTypedQuery<any, any>>;\n    if (typeof query_sql === 'function') {\n      const tables = this.db.getFromBuilder<RemoteModule & UntypedSchemaDef>();\n      const result = query_sql(tables);\n      queries = Array.isArray(result) ? result : [result];\n    } else {\n      queries = Array.isArray(query_sql) ? query_sql : [query_sql];\n    }\n    if (queries.length === 0) {\n      throw new Error('Subscriptions must have at least one query');\n    }\n    const queryStrings = queries.map(q => {\n      if (typeof q === 'string') return q;\n      if (isRowTypedQuery(q)) return toSql(q);\n      throw new Error('Subscriptions must be SQL strings or typed queries');\n    });\n    return new SubscriptionHandleImpl(\n      this.db,\n      queryStrings,\n      this.#onApplied,\n      this.#onError\n    );\n  }\n\n  /**\n   * Subscribes to all rows from all tables.\n   *\n   * This method is intended as a convenience","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/6dee26c6efc2856793e12b148a59742964f5d783/crates/bindings-typescript/src/sdk/subscription_builder_impl.ts#L102-L138","documentation":"SubscriptionBuilderImpl.subscribe normalizes its argument (a SQL string, an array, or a callback returning query/queries) into a query list and requires at least one entry. An empty array — or a callback that returns an empty array — throws before any subscription message is sent.","triggerScenarios":"subscriptionBuilder().onApplied(cb).subscribe([]); or a callback like tables => enabledTables.map(t => tables[t].all()) where enabledTables is empty.","commonSituations":"Building subscriptions dynamically from configuration that can legitimately be empty; a filter step that removes every table; a previously hardcoded single query becoming data-driven.","solutions":["Guard before subscribing: if (queries.length === 0) skip or throw your own config error","If broad subscription is the intent, use subscribeToAllTables() instead","Default the dynamic list to at least one explicit query such as 'SELECT * FROM <core_table>'"],"exampleFix":"// before\nsub.subscribe(enabledTables.map(t => `SELECT * FROM ${t}`)); // [] -> throws\n\n// after\nif (enabledTables.length > 0) {\n  sub.subscribe(enabledTables.map(t => `SELECT * FROM ${t}`));\n}","handlingStrategy":"validation","validationCode":"if (queries.length === 0) {\n  // skip subscribing, or throw a precise config error\n  throw new Error('no queries configured for subscription');\n}\nsub.onApplied(onApplied).subscribe(queries);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Guard dynamically-built query lists for emptiness before subscribing","Use subscribeToAllTables() when the intent is broad coverage","Log the query list length at subscription time in debug builds"],"tags":["subscription","query","typescript","argument-validation"],"backgroundTag":"empty-subscription-queries","analyzedSha":"6dee26c6efc2856793e12b148a59742964f5d783","analyzedAt":"2026-08-20T06:08:37.179Z","contentChangedAt":"2026-08-20T06:08:37.179Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}