# Classic ASP / VBScript Coding Rules _Live from BN_CodingRules. Follow these when writing new .asp files._ ## VBScript Syntax ### 1.1 — Identifiers cannot start with an underscore _[critical]_ **Symptom:** Compilation error 800a0408 "Invalid character" at the line where the identifier appears. **Why:** VBScript rejects any identifier whose first character is an underscore. This applies to variables, functions, parameters, and constants. **DON'T:** ```vbs Dim _ansRaw Function _fmtPrice(p) : ... : End Function ``` **DO:** ```vbs Dim ansRaw Function fmtTicketPrice(p) : ... : End Function ``` _Hit in:_ Hit in _event_placeholders.asp (renamed _fmtPrice -> fmtTicketPrice and _ordinalSuffix -> ordinalSuffix) and earlier in workshop.asp (_ansRaw). ### 1.2 — Single-line If/Then cannot be chained with ElseIf _[critical]_ **Symptom:** Compilation error 800a03f6 "Expected End" at the ElseIf line. **Why:** A single-line "If x Then y" closes the If at end of line. The ElseIf that follows is orphaned. To use ElseIf, every branch must be on its own line with Then at end-of-line, terminated by End If. **DON'T:** ```vbs If h = 0 Then h = 12 : ap = "AM" ElseIf h = 12 Then ap = "PM" Else ap = "AM" End If ``` **DO:** ```vbs If h = 0 Then h = 12 ap = "AM" ElseIf h = 12 Then ap = "PM" Else ap = "AM" End If ``` _Hit in:_ Hit in AIWorkshopOffer.asp FmtTime helper. Note: single-line "If x Then y Else z" WITHOUT ElseIf IS legal. ### 1.3 — Functions return by assigning to the function name _[critical]_ **Symptom:** Either a runtime error or the function returns Empty. **Why:** VBScript has no Return keyword. A function returns by assigning its result to a variable with the same name as the function. **DON'T:** ```vbs Function Double(n) Return n * 2 End Function ``` **DO:** ```vbs Function Double(n) Double = n * 2 End Function ``` _Hit in:_ Common gotcha when coming from JS or other languages. Every helper in our codebase follows the assign-to-name pattern. ### 1.4 — VBScript has no native IIf — define your own _[critical]_ **Symptom:** Runtime error 800a000d "Type mismatch: IIf". **Why:** Unlike VB/VBA, VBScript does not ship with an IIf function. Calling IIf without a local definition throws Type mismatch at runtime. **DON'T:** ```vbs x = IIf(cond, valTrue, valFalse) 'without a local IIf definition ``` **DO:** ```vbs Function IIf(cond, valT, valF) If cond Then IIf = valT Else IIf = valF End Function ``` _Hit in:_ Hit in Board.asp. Defined a local IIf helper at the top of the file to fix it. ## Null Handling ### 2.1 — Null & "" = Null (not "") _[critical]_ **Symptom:** Downstream string operations like Len, Left, Mid, InStr throw Type mismatch. **Why:** VBScript preserves Null through string concatenation. Concatenating Null with "" does NOT coerce to "". You must explicitly check with IsNull. **DON'T:** ```vbs Dim x : x = dict(key) If Len(x) > 0 Then ... 'throws Type mismatch if x is Null ``` **DO:** ```vbs Dim x : x = dict(key) & "" If IsNull(x) Then x = "" ``` _Hit in:_ Hit multiple times in workshop.asp (pills_multi block) and in _event_placeholders.asp. ### 2.2 — Always coerce recordset field reads with & "" _[critical]_ **Symptom:** Any NULL column poisons downstream concat/string ops. **Why:** Reading rs("FieldName") returns a Variant. NULL columns come back as the Null value, not an empty string. Append & "" to coerce to String. **DON'T:** ```vbs evName = rs("EventName") 'fragile — breaks if EventName is NULL ``` **DO:** ```vbs evName = rs("EventName") & "" ``` _Hit in:_ Standard pattern across all our .asp files. Every single rs(...) read in GetEventDetail.asp, SaveEvent.asp, etc. uses & "". ### 2.3 — Mid/Left/InStr on non-string Variants throws Type mismatch _[critical]_ **Symptom:** Runtime error 800a000d "Type mismatch" on the Mid/Left/InStr call. **Why:** Dictionary returns from NTEXT columns or certain Variant types pass IsNull/IsEmpty but still blow up on string ops. Coerce defensively with & "" AND wrap in On Error Resume Next. **DON'T:** ```vbs pipeP = InStr(1, existing, "|", 1) 'throws if existing is a non-string Variant ``` **DO:** ```vbs On Error Resume Next Dim raw : raw = existing & "" If IsNull(raw) Then raw = "" If Len(raw) > 0 Then pipeP = InStr(1, raw, "|", 1) If Err.Number <> 0 Then Err.Clear : raw = "" On Error GoTo 0 ``` _Hit in:_ Hit multiple times in workshop.asp pills_multi block (line 556). The dictionary returned NTEXT content as an odd Variant. ### 2.4 — CDate can throw Type mismatch even when IsDate is True _[critical]_ **Symptom:** Runtime error 800a000d "Type mismatch" on WeekdayName/Weekday/MonthName after a CDate. **Why:** Some DB date formats pass IsDate() but fail CDate() downstream (localization quirks, odd formats). Wrap casts in On Error Resume Next. **DON'T:** ```vbs dateLong = WeekdayName(Weekday(CDate(evDate)), False) & " " & ... 'no guard ``` **DO:** ```vbs Dim dCast On Error Resume Next If IsDate(evDate) Then dCast = CDate(evDate) If Err.Number = 0 Then dateLong = WeekdayName(Weekday(dCast), False) & ", " & MonthName(Month(dCast), False) & " " & Day(dCast) & ", " & Year(dCast) End If End If If Err.Number <> 0 Then Err.Clear On Error GoTo 0 ``` _Hit in:_ Hit in AIWorkshopOffer.asp line 105. IsDate(evDate) returned True but the chained CDate/Weekday call threw Type mismatch anyway. ### 2.5 — Read migration-added columns defensively _[warning]_ **Symptom:** Runtime error "Item cannot be found in the collection corresponding to the requested name" — often crashes the page in dev/staging where the migration hasn't run yet. **Why:** When a new column is added via migration, other environments may not have it yet. Always wrap reads in On Error Resume Next until the migration is universally deployed. **DON'T:** ```vbs evSub = rs("Subtitle") & "" 'crashes if the Subtitle column migration hasn't run yet ``` **DO:** ```vbs Dim evSub : evSub = "" On Error Resume Next evSub = rs("Subtitle") & "" If Err.Number <> 0 Then evSub = "" : Err.Clear On Error GoTo 0 ``` _Hit in:_ Pattern used in GetEventDetail.asp for the new Subtitle column (added 2026-04-29). ## Database ### 3.1 — MARS deadlock — one open recordset per connection _[critical]_ **Symptom:** Queries silently return nothing, or the page renders empty JSON. No error is thrown. **Why:** SQL Server without MARS allows only one open recordset per connection. Calling a helper that opens its own recordset while a parent rs is still open deadlocks or returns empty. **DON'T:** ```vbs Set rs = conn.Execute("SELECT ...") resolved = ResolveEventPlaceholders(rs("Description") & "", id, conn) 'BREAKS — rs still open ``` **DO:** ```vbs rawDesc = rs("Description") & "" jsonOut = jsonOut & """description"":""__DESC_PLACEHOLDER__""," rs.Close : Set rs = Nothing 'Now safe to call helpers that open their own recordsets: Dim resolved : resolved = ResolveEventPlaceholders(rawDesc, id, conn) jsonOut = Replace(jsonOut, "__DESC_PLACEHOLDER__", JSONEsc(resolved)) ``` _Hit in:_ Hit in GetEventDetail.asp — EditEvent.asp and Eventdetails.asp were loading blank because ResolveEventPlaceholders was called while rs was still open. Fix: capture raw, close rs, call helper, stitch result back with Replace. ### 3.2 — Always close recordsets and null the reference _[warning]_ **Symptom:** Connection pool leaks, slow queries over time, occasional "connection busy" errors. **Why:** ADO recordsets hold connection resources. Close + Set Nothing frees them immediately instead of waiting for GC. **DON'T:** ```vbs '...no close... just let the rs go out of scope ``` **DO:** ```vbs rs.Close : Set rs = Nothing ``` _Hit in:_ Every recordset read in the codebase should be followed by this pair. ### 3.3 — Always SQL-escape user values before building SQL _[critical]_ **Symptom:** SQL injection vulnerability + runtime SQL syntax errors on values containing apostrophes. **Why:** Build a SqlSafe helper that doubles single quotes. Use it on every value that goes into a SQL string. **DON'T:** ```vbs sql = "UPDATE BN_Events SET EventName = '" & evName & "' WHERE ..." 'unsanitized ``` **DO:** ```vbs Function SqlSafe(str) : SqlSafe = Replace(str & "", "'", "''") : End Function sql = "UPDATE BN_Events SET EventName = '" & SqlSafe(evName) & "' WHERE ..." ``` _Hit in:_ SqlSafe is defined in SaveEvent.asp and used across all our UPDATE/INSERT statements. ### 3.4 — Clamp form input lengths to column width before SQL _[warning]_ **Symptom:** SQL error "String or binary data would be truncated" when a user pastes something longer than the column width. **Why:** Before using a form value in SQL, check Len() and truncate with Left() to match the column definition. **DON'T:** ```vbs 'no length check before UPDATE BN_Events SET Subtitle = '...' ``` **DO:** ```vbs If Len(evSubtitle) > 255 Then evSubtitle = Left(evSubtitle, 255) ``` _Hit in:_ Pattern used in SaveEvent.asp for the Subtitle NVARCHAR(255) column. ### 3.5 — Migrations must be idempotent _[warning]_ **Symptom:** Re-running a migration throws "column already exists" or duplicates seed data. **Why:** Every schema change must be guarded by IF NOT EXISTS / IF EXISTS. Every seed UPDATE should be guarded by a WHERE that only matches when the field is currently empty/null. **DON'T:** ```vbs ALTER TABLE dbo.BN_Events ADD Subtitle NVARCHAR(255) NULL 'fails on re-run ``` **DO:** ```vbs IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID('dbo.BN_Events') AND name = 'Subtitle') BEGIN ALTER TABLE dbo.BN_Events ADD Subtitle NVARCHAR(255) NULL END ``` _Hit in:_ Pattern used in _migrations/2026-04-29_event_subtitle.sql. ### 3.6 — Admin check on BN_Members is MemberRole='admin', NOT a boolean IsAdmin column _[critical]_ **Symptom:** SQL error 80040e14 "Invalid column name 'IsAdmin'" when querying BN_Members for an admin check. **Why:** The BN_Members table uses a text MemberRole column (values: "member", "admin", possibly others) - there is no IsAdmin bit column. Don't invent one. Every page that needs to gate on admin status must read MemberRole. **DON'T:** ```vbs Set rs = conn.Execute("SELECT ISNULL(IsAdmin,0) AS IsAdmin FROM BN_Members ...") 'column does not exist ``` **DO:** ```vbs Set rs = conn.Execute("SELECT ISNULL(MemberRole,'member') AS MemberRole FROM BN_Members WHERE MemberID = " & CLng(mid)) If Not rs.EOF Then isAdmin = (LCase(Trim(rs("MemberRole") & "")) = "admin") ``` _Hit in:_ Hit on MemberVlog.asp, AdminVlog.asp, Vlog_api.asp on 2026-04-29. The canonical pattern lives in Shoutouts.asp lines 10-21. ## File Organization ### 4.1 — Define helper Functions at the TOP of the ASP file _[warning]_ **Symptom:** Inconsistent behavior — the function works in some places but returns Empty in others. **Why:** VBScript function hoisting is reliable within a single <% %> block but inconsistent when functions are defined in one block and called from HTML-inline <%= ... %> blocks lower in the file. Putting helpers in the first <% %> block avoids this. **DON'T:** ```vbs 'helpers defined near the BOTTOM of the file, called from inline <%= ... %> at the top ``` **DO:** ```vbs <%@ Language=VBScript %> <% Response.Buffer = True %> <% 'ALL HELPERS HERE: Function HE(s) : ... : End Function Function JSReady(s) : ... : End Function '...then data loading, then render %> ``` _Hit in:_ Hit in EventSignup.asp — JSReadyName was defined at the bottom but called in a 'any quote in the name kills the page ``` **DO:** ```vbs Function JSReady(s) If IsNull(s) Then JSReady = "" : Exit Function Dim t : t = s & "" t = Replace(t, "\", "\\") t = Replace(t, """", "\""") t = Replace(t, Chr(10), " ") t = Replace(t, Chr(13), " ") JSReady = t End Function ``` _Hit in:_ Used in EventSignup.asp for prefilling name from cookie. ## Cookies & Auth ### 8.1 — Request.Cookies — coerce with & "" and always validate _[critical]_ **Symptom:** Missing cookies return an empty Variant that trips type ops. Client-controlled values can inject SQL. **Why:** Always coerce to string, Trim, AND validate (numeric check, membership lookup, etc.) before using in SQL or logic. **DON'T:** ```vbs mid = Request.Cookies("sde_mid") sql = "... WHERE MemberID = " & mid ``` **DO:** ```vbs existingMid = Trim(Request.Cookies("sde_mid") & "") If existingMid <> "" And IsNumeric(existingMid) Then 'safe to use CLng(existingMid) now End If ``` _Hit in:_ Used in EventSignup.asp, Memberdashboard.asp, ScanConnect.asp, MyProspects.asp. ## Error Handling ### 9.1 — Master defensive pattern — never let data crash the page _[critical]_ **Symptom:** A data anomaly (NULL, odd date format, missing column) 500s the whole page instead of rendering a graceful fallback. **Why:** Wrap any operation that could throw (CDate, risky field reads, string ops on Variants) in On Error Resume Next, check Err.Number, fall back to a sensible default, Err.Clear, On Error GoTo 0. **DON'T:** ```vbs 'call risky_operation() with no guards ``` **DO:** ```vbs On Error Resume Next Dim result : result = "" result = risky_operation() If Err.Number <> 0 Then result = "" Err.Clear End If On Error GoTo 0 ``` _Hit in:_ Used around CDate in AIWorkshopOffer.asp and EventSignup.asp; around Mid/Left/InStr in workshop.asp; around rs(col) for new migration columns.