# 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. ### 1.5 — Never Dim the same variable twice in one Sub/Function _[critical]_ **Symptom:** Compilation error 800a0411 "Name redefined" at the second Dim line. The whole file fails to compile so the page 500s. **Why:** VBScript treats a duplicate Dim inside the same procedure scope as a hard compile error. Most other languages let you re-declare; VBScript does not. Easy to hit when you copy-paste a block from elsewhere in the file and the variable was already Dim'd earlier in the same Sub. **DON'T:** ```vbs Sub sendThemedConfirmation() Dim host : host = Request.ServerVariables("HTTP_HOST") ' ... 30 lines later ... Dim host ' second Dim — compile error End Sub ``` **DO:** ```vbs Sub sendThemedConfirmation() Dim host : host = Request.ServerVariables("HTTP_HOST") ' ... 30 lines later ... host = Request.ServerVariables("HTTP_HOST") ' just assign, do not re-Dim End Sub ``` _Hit in:_ Hit in request_submit.asp on 2026-05 — Dim host appeared at lines 542 and 574 inside the same Sub. Removed the second Dim and the file compiled. ### 1.6 — Use CDbl/CCur for large numbers — CLng overflows at ~2.1 billion _[critical]_ **Symptom:** Runtime error 800a0006 "Overflow" on CLng() when the input number is above 2,147,483,647 (or below -2,147,483,648). **Why:** CLng casts to a 32-bit signed integer. Phone numbers, Stripe amounts in cents, Twilio IDs, large timestamps and many other "numeric-looking" strings exceed Long range and overflow. CDbl handles any size; CCur is correct for currency math. **DON'T:** ```vbs amount = CLng(Request.Form("amount_cents")) ' overflows on anything >= $21.47M phoneInt = CLng(rawPhone) ' overflows on most full-format phone numbers ``` **DO:** ```vbs If IsNumeric(rawAmount) Then amount = CDbl(rawAmount) ' arbitrary magnitude, OK for IDs ' or, for money math: amount = CCur(rawAmount) ' fixed 4-decimal currency type End If ``` _Hit in:_ Hit in CPG property edit pricing on 2026-05. Default to CDbl unless you specifically need 32-bit truncation. ### 1.7 — Don't colon-pack If/Else across a function body _[critical]_ **Symptom:** Compilation error 800a03f4 'Expected If' or 800a0401 'Expected end of statement' when the page loads — sometimes only on IIS, not when you eyeball the file. **Why:** VBScript's colon separator only works for the SINGLE-LINE If syntax (If x Then a = 1 Else a = 2). The moment you try to wrap a multi-keyword block (Function, Sub, Do, For) with colons, the parser can't tell where the block ends. Pasting from one-line examples is the common trigger. **DON'T:** ```vbs Function HE(s) : If IsNull(s) Then HE = "" Else HE = Server.HTMLEncode(s & "") : End Function ``` **DO:** ```vbs Function HE(s) If IsNull(s) Then HE = "" Else HE = Server.HTMLEncode(s & "") End If End Function ``` _Hit in:_ ManageCodingRules.asp top-of-file helpers. Multiple ASP files across BN + freestylerslegacy. ## 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). ### 2.6 — Always wrap recordset values with HE() or HA() before output _[critical]_ **Symptom:** Type mismatch 'CStr' (800a000d) when a column is NULL. Or worse — silent '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. ### 7.4 — Inline form values must be HTMLEncoded — including value="" attributes _[warning]_ **Symptom:** Form fields render broken (closing quotes early) or empty when the loaded value contains a double-quote, ampersand, or angle bracket. XSS surface if the value came from user input. **Why:** Server.HTMLEncode handles element text; HA() (attribute-safe escape) is needed inside value="...", placeholder="...", and other attributes. Easy to forget when round-tripping edit forms. **DON'T:** ```vbs "> ' breaks if Title contains " & < or anything HTML-significant ``` **DO:** ```vbs "> ' HA escapes & " < and treats Null as "" ``` _Hit in:_ Pattern used in ManageCodingRules.asp, EditEvent.asp, edituser.asp. HE for element text, HA for attribute values. ### 7.5 — Don't scope base text/background rules to #editor only _[critical]_ **Symptom:** Page looks fine inside the CMS preview/editor, then ships dark/cream/blank on the live site — headlines and body copy go invisible against the section background. **Why:** CMS page builders wrap content in #editor while editing but render it inside .page-wrapper (or no wrapper) when live. Any rule scoped only to #editor stops matching the moment the page leaves the editor, so the cascade never delivers your base color or background. **DON'T:** ```vbs #editor { color: #fff; background: var(--navy-deep); font-family: 'DM Sans',sans-serif; } ``` **DO:** ```vbs #editor, .page-wrapper { color: #fff !important; background: var(--navy-deep) !important; font-family: 'DM Sans', sans-serif; } ``` _Hit in:_ rankingmastery/clients-html/frank.html. Same pattern hits every CMS-embedded page. ### 7.6 — Default state of every element must be visible (no JS-dependent reveals) _[critical]_ **Symptom:** Entire sections (reviews, cards, panels, banners) are blank on the live page. Inspector shows the text is there with the right color — it's just opacity:0 and never animates in. **Why:** The page builder strips inline ``` **DO:** ```vbs .fl-sr { opacity: 1; transform: none; /* If you really want a reveal, use pure CSS @keyframes, never a JS class-adder: */ animation: fadeUp .7s ease both; } @keyframes fadeUp { from { opacity:0; transform:translateY(20px); } to { opacity:1; transform:none; } } ``` _Hit in:_ rankingmastery/clients-html/frank.html — the .fl-sr opacity:0 bug. Same root cause as embed-page-prompt.md rule 2. ### 7.7 — Override the CMS .widget-container background, not just the inner form fields _[warning]_ **Symptom:** A form (or any widget) renders as a bright white panel on top of your dark card. The labels and inputs you styled for a dark theme are invisible against the white. **Why:** The CMS injects a parent
with a hardcoded white-gradient inline background that sits on top of whatever card you wrap it in. Styling only the inputs and labels doesn't help — the outer container is what's covering your card. **DON'T:** ```vbs .fl-form-card input { background: rgba(255,255,255,0.08); color: #fff; } .fl-form-card label { color: rgba(255,255,255,0.85); } ``` **DO:** ```vbs .fl-form-card .widget-container, .fl-form-card .widget-container.widget-optin { background: transparent !important; padding: 0 !important; border-radius: 0 !important; box-shadow: none !important; } .fl-form-card input { background: rgba(255,255,255,0.08); color: #fff; } .fl-form-card label { color: rgba(255,255,255,0.85); } ``` _Hit in:_ rankingmastery/clients-html/frank.html form-widget fix. Same shape as the embed-page-prompt rule 5. ### 7.8 — Never paste the page-wrapper + fonts block more than once _[warning]_ **Symptom:** Page renders fine but Inspector shows 4–6 nested
elements with duplicated Google Fonts tags. Hidden cost: pages weigh 3-5× what they should and layout math (vh, %, flex) misbehaves. **Why:** Copy-pasting the "starter template" into a CMS that already injects the same boilerplate doubles up the wrapper. Each subsequent paste nests deeper. The CMS doesn't complain — fonts still load, divs still render — but everything is fragile. **DON'T:** ```vbs
``` **DO:** ```vbs
...
...
``` _Hit in:_ RankingMastery SEO page generator. Strip nested wrappers in the renderer before writing the file. ### 7.9 — Don't rely solely on a Font Awesome kit URL — add a public CDN fallback _[warning]_ **Symptom:** Every renders as a blank circle/square on the live site, even though the same page worked yesterday. DevTools Network tab shows the kit script returning 401/403. **Why:** Font Awesome kits (kit.fontawesome.com/XYZ.js) are domain-locked to the URLs whitelisted in your Font Awesome account. The moment the page renders on a new subdomain or a staging URL, the kit silently fails — IIS doesn't even log it because the failure is in the browser. **DON'T:** ```vbs ``` **DO:** ```vbs ``` _Hit in:_ rankingmastery/menu.asp — kit silently 401'd on a new subdomain. Both BMX and FL hit this. ## 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. ### 8.2 — Admin pages start with an explicit Session("account_id") check that 302s on fail _[critical]_ **Symptom:** A guest hits /BusinessNetworking/ManageCodingRules.asp directly and gets the full admin form. Or a logged-in non-admin can view another tenant's data because the page only filters by Session("account_id") in the WHERE clause but never verifies it's the right user type. **Why:** It's easy to assume the menu link is the auth — but URLs are guessable. Without an explicit top-of-file gate, anyone with the URL can hit admin endpoints. And a missing account_id check means the SQL runs as whatever Session has (often empty), exposing default-tenant data. **DON'T:** ```vbs <% ' (no auth check, page just runs) Set rs = conn.Execute("SELECT * FROM BN_CodingRules") %> ``` **DO:** ```vbs <% Dim adminID : adminID = Session("account_id") If adminID = "" Or Not IsNumeric(adminID) Then Response.Redirect "/BusinessNetworking/login.asp?return=" & Server.URLEncode(Request.ServerVariables("URL")) Response.End End If ' Optional second gate for super-admin only: If CStr(adminID) <> "8748" Then Response.Status = "403 Forbidden" Response.Write "Not authorized." Response.End End If %> ``` _Hit in:_ ManageCodingRules.asp, menu-admin.asp, every Manage*.asp page. ## 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. ### 9.2 — CDO.Message — set HTMLBody before touching HTMLBodyPart.Charset _[critical]_ **Symptom:** Runtime error "Object required: 'HTMLBodyPart'" when sending email, or the email sends as garbled UTF-8 with no charset set. **Why:** CDO's HTMLBodyPart is a derived COM object — it does NOT exist until you assign HTMLBody. Setting .Charset on it before HTMLBody is like dereferencing a null pointer. The endpoint then returns "bad_response" because the function bails before writing JSON. **DON'T:** ```vbs Set msg = CreateObject("CDO.Message") msg.HTMLBodyPart.Charset = "utf-8" ' Object required — HTMLBodyPart doesn't exist yet msg.HTMLBody = htmlBody ``` **DO:** ```vbs Set msg = CreateObject("CDO.Message") msg.HTMLBody = htmlBody ' assign FIRST msg.HTMLBodyPart.Charset = "utf-8" ' now safe — body part exists msg.BodyPart.Charset = "utf-8" ' optional: plain-text part charset too ``` _Hit in:_ Hit in cdoSend in request_submit.asp on 2026-05. The wrong order swallowed the email send, and an "On Error GoTo 0" further down meant the bubble-up turned into a 500 with a 1-byte response body. ### 9.3 — Call Response.Clear before writing JSON error output _[warning]_ **Symptom:** Client-side fetch reports "bad_response" / "Unexpected token < in JSON". Your endpoint did write valid JSON, but a stray <%= ... %> or include leaked HTML into the response buffer earlier and the JSON parser chokes on it. **Why:** Response.Buffer=True keeps all writes pending until flush. Any upstream stray output (include side effects, ASP error pages, a #include that emits whitespace) lives in the buffer. emitError adds JSON on top, so the client sees HTML-then-JSON. **DON'T:** ```vbs Sub emitError(msg) Response.ContentType = "application/json" Response.Write "{""ok"":false,""error"":""" & JSONEsc(msg) & """}" Response.End End Sub ' Any buffered HTML/whitespace ends up prefixing the JSON ``` **DO:** ```vbs Sub emitError(msg) Response.Clear ' drop anything upstream buffered Response.ContentType = "application/json" Response.Charset = "UTF-8" Response.Write "{""ok"":false,""error"":""" & JSONEsc(msg) & """}" Response.End End Sub ``` _Hit in:_ Hit on request_submit.asp 2026-05. Added Response.Clear so the client always sees a clean JSON body even when something upstream emitted HTML. ### 9.4 — Keep On Error Resume Next scope tight — never re-enable strict errors mid-Sub _[warning]_ **Symptom:** A Sub that "had OERN at the top" still throws an unhandled 500 deep inside. Adding "On Error GoTo 0" mid-function unexpectedly re-enables strict error trapping for everything after that line, including CDO/COM/SQL calls that assumed OERN was still active. **Why:** OERN is a procedure-level switch, but "On Error GoTo 0" turns it OFF anywhere it appears — including in the middle of the same Sub. Code added later (CDO send, recordset open, JSON build) that assumes "we're in OERN" will then crash hard. **DON'T:** ```vbs Sub cdoSend(...) On Error Resume Next ' ... CDO setup ... On Error GoTo 0 ' OERN now OFF for rest of Sub msg.Send ' any CDO error here is now an unhandled 500 End Sub ``` **DO:** ```vbs Sub cdoSend(...) On Error Resume Next ' ... entire Sub ... msg.Send If Err.Number <> 0 Then errDesc = "CDO send failed: " & Err.Description Err.Clear End If ' On Error GoTo 0 is at the CALLER, not inside the Sub End Sub ``` _Hit in:_ Hit in cdoSend / request_submit.asp 2026-05. The mid-function On Error GoTo 0 turned a recoverable CDO failure into a hard 500 + bad_response on the client.