Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Jenkins By James Joyner IV · · 10 min read

Jenkins Error: 'java.io.NotSerializableException' — Cause, Fix, and Troubleshooting Guide

Quick answer

Fix Jenkins pipeline 'java.io.NotSerializableException': stop holding matchers, LazyMap, or streams across steps in CPS Groovy using @NonCPS and local scope.

  • #jenkins
  • #ci-cd
  • #troubleshooting
  • #errors
Free toolkit

Fixing errors like this? Get 500 free DevOps AI prompts

500 copy-paste AI prompts for the stack you actually run — one PDF, free.

Overview

A Jenkins Pipeline failed because the CPS (Continuation-Passing Style) engine tried to serialize the running program’s state and hit an object that cannot be serialized. Pipelines are durable: at every step boundary (sh, checkpoint, input, stash, an agent hop) Jenkins saves the entire program — every local variable currently in scope — to disk so a build can survive a controller restart. If a variable holds something non-serializable at that moment, you get:

java.io.NotSerializableException: java.util.regex.Matcher
	at org.jenkinsci.plugins.workflow.support.pickles.serialization.RiverWriter...
	at ...
Caused: an attempt to serialize the build's program state failed

The exception names the offending class — java.util.regex.Matcher, groovy.json.internal.LazyMap, java.io.InputStream, and so on. These are objects that must not be alive across a step. This guide is about that step-boundary serialization of local objects; for failures rooted in global variables and shared-library binding, see the Groovy CPS not serializable guide, which covers that adjacent case.

Symptoms

  • A pipeline fails right after (or during) a sh, stash, input, or checkpoint step with NotSerializableException.
  • The message names a specific class: java.util.regex.Matcher, groovy.json.internal.LazyMap, java.io.InputStream, java.io.File, a JDBC connection, etc.
  • The same code runs fine in the Groovy console or a plain script but fails in a Jenkins Pipeline.
  • Moving the offending logic to before the first sh step (or wrapping it) makes the error disappear.
  • The failure is deterministic — same line, same class, every run.

Common Root Causes

  • A regex Matcher held across a stepdef m = text =~ /.../ creates a java.util.regex.Matcher, which is not serializable; if m is still in scope at the next step, serialization fails.
  • JsonSlurper LazyMap/LazyList retained across a stepreadJSON/new JsonSlurper().parseText(...) returns lazy, non-serializable collections that must not survive a step boundary.
  • An open stream, reader, or file handle in scopeInputStream, Reader, sockets, and JDBC connections are inherently non-serializable and cannot be carried across steps.
  • Objects from @NonCPS-only libraries leaking into CPS scope — a helper returns a non-serializable object and the caller stores it in a CPS-visible local.
  • Wide variable scope — declaring a heavy/temporary object at pipeline or stage scope so it stays alive far longer than the few lines that use it.
  • Non-serializable field on a shared-library class — a Serializable step class holds a transient-worthy field (a client, a matcher) that is not marked transient.

How to diagnose

Read the class name in the exception — it points straight at the offending object type. Then find where that type is created and whether a step runs while it is still in scope.

Reproduce with a minimal Pipeline to confirm the pattern (a Matcher held across sh):

node {
  def m = ('build-42' =~ /build-(\d+)/)   // java.util.regex.Matcher
  sh 'echo crossing a step boundary'       // <-- serialization happens here
  echo m[0][1]                             // m still in scope => NotSerializableException
}

Check the full stack trace in the build’s Console Output and, for CPS internals, the pipeline’s Replay view to iterate quickly. In the Script Console you can confirm which classes are serializable:

// Quick serializability check (Script Console)
[java.util.regex.Matcher, groovy.json.internal.LazyMap, java.util.HashMap].each { c ->
  println "${c.name}\tSerializable=${Serializable.isAssignableFrom(c)}"
}

Search your shared library and Jenkinsfiles for the usual offenders:

grep -RnE '=~|new JsonSlurper|readJSON|new File\(|\.newInputStream|getInputStream' vars/ src/ Jenkinsfile

Fixes

1. Confine a Matcher to a @NonCPS method

Never let a Matcher live across a step. Do the match, extract the plain String/value you need, and drop the Matcher — ideally inside a @NonCPS helper so the CPS transformer never tracks it:

@NonCPS
def extractBuildNumber(String text) {
  def m = (text =~ /build-(\d+)/)
  return m ? m[0][1] : null   // Matcher is local to this method; only a String escapes
}

node {
  def n = extractBuildNumber('build-42')
  sh 'echo crossing a step boundary'
  echo "build number: ${n}"   // n is a String — safe to serialize
}

A @NonCPS method runs to completion without CPS instrumentation, so objects created inside it are never candidates for cross-step serialization — but you must not call pipeline steps (sh, echo) from inside a @NonCPS method.

2. Convert lazy JSON into plain, serializable collections immediately

readJSON/JsonSlurper returns lazy maps and lists. Deep-copy them into ordinary HashMap/ArrayList before any step, or parse inside a @NonCPS method and return primitives:

@NonCPS
Map parseConfig(String json) {
  def lazy = new groovy.json.JsonSlurper().parseText(json)
  // Materialize into a plain, serializable structure
  return new HashMap(lazy as Map)
}

node {
  def cfg = parseConfig(readFile('config.json'))
  sh "deploy --env ${cfg.environment}"   // cfg is a plain HashMap — serializes fine
}

If you use the Pipeline Utility Steps readJSON, wrap the result the same way, or read only the fields you need into String/number locals.

3. Scope temporary objects tightly and null them before a step

Keep non-serializable temporaries inside the smallest block that uses them so they are out of scope before the next step. Do not declare them at pipeline/stage level:

node {
  String firstLine
  // stream is used and closed inside this block; never crosses a step
  new File(pwd(), 'report.txt').withReader { r -> firstLine = r.readLine() }
  sh 'echo now safe to cross a step'
  echo firstLine
}

Prefer pipeline steps over raw Java I/O where possible — readFile/writeFile return String, which is serializable, instead of a live handle.

4. Mark non-serializable fields transient in shared-library classes

If a shared-library class must implement Serializable (so it can be a step or field), mark any heavy or non-serializable field transient and lazily rebuild it:

class ApiClient implements Serializable {
  private final String baseUrl
  private transient HttpURLConnection conn   // not serialized; rebuilt on demand
  ApiClient(String baseUrl) { this.baseUrl = baseUrl }
}

5. Do heavy processing on the agent, not in pipeline scope

If you are parsing large output, push it to a subprocess and read back only the small result, which sidesteps CPS serialization entirely:

node {
  sh 'grep -c ERROR build.log > count.txt'
  int errors = readFile('count.txt').trim().toInteger()
  echo "errors: ${errors}"
}

What to watch out for

  • Any object created by =~ is a Matcher — it is the single most common cause. Use ==~ for a boolean match, or extract the value in a @NonCPS method.
  • @NonCPS methods must never call pipeline steps (sh, echo, node). They run outside CPS, so steps are unavailable and behavior is undefined.
  • Distinguish this from the global-variable/binding case — if the class named is your own script or a shared global, read the Groovy CPS not serializable guide instead.
  • The error only fires when a step boundary is crossed while the object is in scope; code that never hits a step can look deceptively fine until you add a sh.
  • readFile/writeFile/readJSON results are safer than raw java.io.* — prefer pipeline steps, and materialize lazy structures early.
  • The Jenkins guides hub has more pipeline-scripting fixes.
Free download · 368-page PDF

Get 500 Battle-Tested DevOps AI Prompts — Free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.