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