Article
· Sep 10 4m read

Learning ObjectScript as a New Developer: What I Wish I Knew

I joined InterSystems less than a year ago. Diving into ObjectScript and IRIS was exciting, but also full of small surprises that tripped me up at the beginning. In this article I collect the most common mistakes I, and many new colleagues, make, explain why they happen, and show concrete examples and practical fixes. My goal is to help other new developers save time and avoid the same bumps in the road.

 

1. Getting lost among system classes and where to start

The issue: ObjectScript/IRIS ships with many system classes and packages (%Library, %SYS, %Persistent, %SQL, etc.). As a new dev, it’s hard to know which class or pattern fits a task. I remember trying to find an appropriate persistent class and being unsure whether to extend %Persistent or use a registry-type class.

Why it matters: Picking the wrong approach early makes code harder to maintain and integrates poorly with IRIS features (indexes, security, SQL).

Tip: Start from the concrete need (store records? expose SQL? share objects across processes?) and then search the Class Reference for the relevant capability (persistence, indexes, collections). Use Studio or the InterSystems VS Code extension to browse classes interactively and inspect examples.

 

2. Documentation overwhelm: not knowing the right keywords

The issue: The docs are comprehensive, but if you don’t know the correct class name or term, searches return many unrelated pages. Early on I often spent a lot of time because I didn’t yet know the canonical terms.

Why it matters: You waste time and might implement suboptimal patterns.

Tip:

  • Search the Developer Community for practical examples (search by “persistent class example”, “ObjectScript transaction TSTART example”, etc.).
  • Use the VS Code InterSystems extension, it can jump directly to class definitions.
  • When searching docs, combine class names and actions: e.g. "%Persistent property index example".

 

3. Forgetting .. for local method call

The issue: Calling a method of the same class without .. results in a call not being recognized at runtime.

Wrong:

Class MyClass Extends %Persistent { 
   Method DoWork() 
   { 
       Do Hello()  // wrong: this is not a local method invocation
   } 
   Method Hello()
   { 
       Write "Hello", ! 
   } 
}

Right:

Method DoWork() {
   Do ..Hello()  // correct: local method call 
}

Tip: When you get “Unknown routine” errors for methods you see in the class, check whether you used .. for self-calls.

 

4. Confusing globals and local variables

The issue: ObjectScript distinguishes globals (persistent across sessions, e.g. ^MyGlobal) and local variables (in-memory, scope-limited). New developers frequently use one when the other is intended.

SET localVar = 10    // exists only during the current process/session
SET ^globalVar = 20  // persists in the database across processes and sessions

Tip: Use persistent classes for data that should be stored long-term. Limit use of globals to very specific low-level needs or when you understand consequences.

 

5. Hardcoding globals instead of using persistent classes

The issue: My instinct at first was “quick and dirty”: write to ^MyGlobal directly. That works, but it bypasses class-based benefits: schema, indices, SQL access, and safety.

Better approach:

Class Person Extends %Persistent 
{ 
    Property Name As %String; 
    Property DOB As %Date; 
}

Tip: Prefer %Persistent classes for application data. They give you a cleaner model and integrate with SQL and indexes.

 

6. Ignoring transactions (TSTART / TCOMMIT)

The issue: Developers sometimes perform several writes thinking they are atomic. Without explicit transaction control, partial failures can leave inconsistent data.

TSTART 
// multiple updates 
TCOMMIT

Tip: Identify logical units of work and wrap them in transactions. If something can fail halfway, use TSTART / TROLLBACK / TCOMMIT. Keep in mind that IRIS supports nested transactions: multiple TSTART calls increase the transaction level, and all levels must be committed (or rolled back) before changes are final.

 

7. Misunderstanding SQL options: Embedded SQL vs %SQL.Statement

The issue: Embedded SQL (UPDATE, SELECT blocks inside ObjectScript) and the %SQL.Statement API are both available; choosing without knowing pros/cons can cause awkward code.

Guideline: Use Embedded SQL for fixed/static queries inside routines; use %SQL.Statement when you need to build and execute SQL dynamically.

 

8. Skipping proper error handling

The issue: Not using TRY/CATCH or signaling errors properly makes debugging and reliability harder.

TRY 
{ 
   // code that may fail 
} CATCH ex 
{  
   Write "Error: ", ex.DisplayString(),! 
}

Tip: Wrap risky operations (I/O, external calls, dynamic SQL) with error handlers and log informative messages.

 

Final notes

Early on I wrote globals for convenience and later spent time refactoring to persistent classes. That refactor taught me the value of designing data models up front and using the tools IRIS provides. My best habits now: small experiments, frequent searches on Developer Community, and keeping transactions and error handling in mind from the start.

If you’re new: treat your first few tasks as experiments, create small persistent classes, try simple Embedded SQL, and use the Management Portal and VS Code browser to inspect system classes. Ask questions on the Developer Community; many others had the same “gotchas.”

For more: check the official ObjectScript documentation and the Developer Community for examples and patterns.

 

This article was reviewed and edited with the assistance of AI tools to improve clarity and grammar. The experiences and suggestions described are my own.

Discussion (11)6
Log in or sign up to continue

Combining what you said about error handling and transactions and also returning a status, a very basic outline for a lot of methods could be something like:

try{
    TSTART
    //Do stuff here
    TCOMMIT
    return $$$OK
}
catch ex{
    if $TLEVEL > 0{
        TROLLBACK
    }
    return ex.AsStatus()
}

Some of us who write articles could do a better job of making this easier for you too, though. We like to use short forms of certain things, like {} and [] which are ##class(%Library.DynamicObject) and ##class(%Library.DynamicArray). I try to remember to use the latter in my articles just because it makes it easier to find the thing I'm talking about in the documentation. There are cases like that which are technically correct, but make it harder for beginners to learn.

@David Hockenbroch What about setting that variable (say tlevel) equal to the $TLEVEL at the start of the Try, and then in the Catch, only if $TLEVEL = (tlevel +1) (meaning only your TSTART happened), do a TROLLBACK 1? Because if $TLEVEL is >= 2 more than tlevel, it means that your Try called a method that started its own transaction and didn't correctly commit it or roll it back, and perhaps that method called another method that did the same thing, etc. I think coping with that in your Catch is not really your code's job. Do you concur?

Very nice article

I would add some points:

  1. With regard to selecting a super class (extends)
  • %Persistent - if you want to store data
  • %SerialObject - if you want to provide a structure to be stored as part of a %Persistent class, but never as a separate object. (ie not a forgein reference)
  • %Registered - if you are creating a class that contains utilility methods or other in-memory only structures.  These classes will never store anything in the database. I would also add that IRIS supports multi-inheritence, which many languages do not.  So you can add capabilities to your classes by extending muliple classes.  For example Extends (%Persistent, %XML.Adaptor) creates a class that will support storage to the database and adds XML capabilities like import and export to the class 

   8. error handling

What you indicated is spot on.  I will add understanding and using Exceptions.  If you are working with a new code base using Exceptions for error handling versus returning %Status object is a more modern and better option. If you need to integrate into existing code the direction may be unclear.  In that case returning %Status values may be necessary.

Finally, I would add Post Conditionals that are available on most commands.  This replaces the need to wrap the execution of a single command in an IF statement.  For example, given your code defines an optional feature that is only processed if the feature option flag is turned on.  You could write

if option1 {
    do ##Class(Features.Option1).DoSomething()
}

 OR

do:option1 ##Class(Feature.Option1).DoSomething()