You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sedona.apache.org by ja...@apache.org on 2026/08/10 20:14:36 UTC

(sedona-db) 01/01: refactor(rust/sedona-raster): add RasterError to replace ArrowError plumbing

This is an automated email from the ASF dual-hosted git repository.

james-willis pushed a commit to branch jw/raster-error-type
in repository https://gitbox.apache.org/repos/asf/sedona-db.git

commit 24e92f1b77fa8871f9c8e400969eab7f5c9d38f5
Author: jameswillis <ja...@wherobots.com>
AuthorDate: Mon Aug 10 13:13:56 2026 -0700

    refactor(rust/sedona-raster): add RasterError to replace ArrowError plumbing
    
    The raster crates leaned on arrow_schema::ArrowError as their error type,
    forcing a map_err at every boundary. Introduce a dedicated RasterError
    (mirroring sedona-geometry's SedonaGeometryError, plus an Arrow(#[from])
    variant since raster code calls Arrow constantly) with From bridges into
    DataFusionError and ArrowError and from DataFusionError, and a
    RasterResultExt::context() helper. Migrate the sedona-raster library
    modules to return RasterError and collapse the downstream map_err churn in
    sedona-raster-{functions,gdal,zarr} to ?/.context(). The AsyncRasterLoader
    trait stays on ArrowError to keep that cross-crate/cross-language boundary
    stable; internal-assertion errors still route through sedona_internal_err!.
---
 Cargo.lock                                         |   1 +
 rust/sedona-raster-functions/src/crs_utils.rs      |   8 +-
 rust/sedona-raster-functions/src/rs_dim_band.rs    |  12 +-
 rust/sedona-raster-functions/src/rs_dimensions.rs  |  20 +--
 .../src/rs_set_band_nodata.rs                      |   5 +-
 .../src/rs_set_georeference.rs                     |  11 +-
 rust/sedona-raster-functions/src/rs_slice.rs       |  10 +-
 .../src/rs_spatial_predicates.rs                   |  10 +-
 rust/sedona-raster-functions/src/rs_value.rs       |  27 +--
 rust/sedona-raster-functions/src/rs_values.rs      |  27 +--
 rust/sedona-raster-functions/src/sampling.rs       |   6 +-
 rust/sedona-raster-gdal/src/gdal_common.rs         |  14 +-
 .../src/gdal_dataset_provider.rs                   |  23 ++-
 rust/sedona-raster-gdal/src/mask.rs                |  15 +-
 rust/sedona-raster-gdal/src/rs_as_geotiff.rs       |  16 +-
 rust/sedona-raster-gdal/src/rs_as_raster.rs        |  44 +++--
 rust/sedona-raster-gdal/src/rs_clip.rs             |  30 ++--
 rust/sedona-raster-gdal/src/rs_from_gdal_raster.rs |  19 +-
 rust/sedona-raster-gdal/src/rs_metadata.rs         |  13 +-
 rust/sedona-raster-gdal/src/rs_polygonize.rs       |  28 ++-
 rust/sedona-raster-gdal/src/rs_tile.rs             |  24 ++-
 rust/sedona-raster-gdal/src/rs_zonal_stats.rs      |  13 +-
 rust/sedona-raster-gdal/src/utils.rs               |  72 ++++----
 rust/sedona-raster-zarr/src/loader.rs              |   4 +-
 rust/sedona-raster/Cargo.toml                      |   1 +
 rust/sedona-raster/src/affine_transformation.rs    |  12 +-
 rust/sedona-raster/src/array.rs                    |  30 ++--
 rust/sedona-raster/src/builder.rs                  |  69 ++++----
 rust/sedona-raster/src/error.rs                    | 192 +++++++++++++++++++++
 rust/sedona-raster/src/geo_transform.rs            |  18 +-
 rust/sedona-raster/src/lib.rs                      |   1 +
 rust/sedona-raster/src/traits.rs                   |  66 ++++---
 rust/sedona-raster/src/view_entries.rs             |  32 ++--
 33 files changed, 495 insertions(+), 378 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 34a7121d4..325463499 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6220,6 +6220,7 @@ dependencies = [
  "sedona-common",
  "sedona-schema",
  "sedona-testing",
+ "thiserror 2.0.17",
  "tokio",
 ]
 
diff --git a/rust/sedona-raster-functions/src/crs_utils.rs b/rust/sedona-raster-functions/src/crs_utils.rs
index e18c7a2c6..69650e3b2 100644
--- a/rust/sedona-raster-functions/src/crs_utils.rs
+++ b/rust/sedona-raster-functions/src/crs_utils.rs
@@ -18,8 +18,9 @@
 use std::borrow::Cow;
 
 use datafusion_common::config::ConfigOptions;
-use datafusion_common::{exec_datafusion_err, exec_err, DataFusionError, Result};
+use datafusion_common::{exec_err, DataFusionError, Result};
 use sedona_geometry::transform::{transform, CrsEngine};
+use sedona_raster::error::RasterResultExt;
 use sedona_schema::crs::{deserialize_crs, CoordinateReferenceSystem, Crs, CrsRef};
 use wkb::reader::read_wkb;
 
@@ -73,11 +74,10 @@ pub fn crs_transform_wkb(
 ) -> Result<Vec<u8>> {
     let crs_transform = engine
         .get_transform_crs_to_crs(&from_crs.to_crs_string(), &to_crs.to_crs_string(), None, "")
-        .map_err(|e| exec_datafusion_err!("CRS transform error: {}", e))?;
+        .context("CRS transform error")?;
     let geom = read_wkb(wkb).map_err(|e| DataFusionError::External(Box::new(e)))?;
     let mut out = Vec::with_capacity(wkb.len());
-    transform(geom, crs_transform.as_ref(), &mut out)
-        .map_err(|e| exec_datafusion_err!("Transform error: {}", e))?;
+    transform(geom, crs_transform.as_ref(), &mut out).context("Transform error")?;
     Ok(out)
 }
 
diff --git a/rust/sedona-raster-functions/src/rs_dim_band.rs b/rust/sedona-raster-functions/src/rs_dim_band.rs
index 12c0ab540..34b56ca70 100644
--- a/rust/sedona-raster-functions/src/rs_dim_band.rs
+++ b/rust/sedona-raster-functions/src/rs_dim_band.rs
@@ -20,7 +20,7 @@ use std::sync::Arc;
 use arrow_schema::DataType;
 use datafusion_common::cast::as_string_view_array;
 use datafusion_common::error::Result;
-use datafusion_common::{arrow_datafusion_err, exec_err};
+use datafusion_common::exec_err;
 use datafusion_expr::{ColumnarValue, Volatility};
 use sedona_common::sedona_internal_datafusion_err;
 use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
@@ -102,9 +102,7 @@ impl SedonaScalarKernel for RsDimToBand {
                     )?;
 
                     for band_idx in 0..raster.num_bands() {
-                        let band = raster
-                            .band(band_idx)
-                            .map_err(|e| arrow_datafusion_err!(e))?;
+                        let band = raster.band(band_idx)?;
 
                         let maybe_dim_idx = band.dim_index(name);
                         match maybe_dim_idx {
@@ -228,7 +226,7 @@ impl SedonaScalarKernel for RsBandToDim {
                         return exec_err!("RS_BandToDim: raster has no bands");
                     }
 
-                    let band0 = raster.band(0).map_err(|e| arrow_datafusion_err!(e))?;
+                    let band0 = raster.band(0)?;
                     let ref_dim_names = band0.dim_names();
                     let ref_shape = band0.shape().to_vec();
                     let ref_data_type = band0.data_type();
@@ -246,7 +244,7 @@ impl SedonaScalarKernel for RsBandToDim {
                     }
 
                     for i in 1..num_bands {
-                        let band = raster.band(i).map_err(|e| arrow_datafusion_err!(e))?;
+                        let band = raster.band(i)?;
                         if band.dim_names() != ref_dim_names {
                             return exec_err!(
                                 "RS_BandToDim: band {i} has different dim_names than band 0"
@@ -284,7 +282,7 @@ impl SedonaScalarKernel for RsBandToDim {
 
                     let mut concat_data = Vec::new();
                     for i in 0..num_bands {
-                        let band = raster.band(i).map_err(|e| arrow_datafusion_err!(e))?;
+                        let band = raster.band(i)?;
                         let ndb = band.nd_buffer()?;
                         let data = ndb.as_contiguous()?;
                         concat_data.extend_from_slice(data);
diff --git a/rust/sedona-raster-functions/src/rs_dimensions.rs b/rust/sedona-raster-functions/src/rs_dimensions.rs
index ff4e6e2f1..507f11842 100644
--- a/rust/sedona-raster-functions/src/rs_dimensions.rs
+++ b/rust/sedona-raster-functions/src/rs_dimensions.rs
@@ -21,7 +21,7 @@ use arrow_array::builder::{Int32Builder, Int64Builder, ListBuilder, StringViewBu
 use arrow_schema::DataType;
 use datafusion_common::cast::{as_int32_array, as_string_view_array};
 use datafusion_common::error::Result;
-use datafusion_common::{arrow_datafusion_err, exec_err};
+use datafusion_common::exec_err;
 use datafusion_expr::{ColumnarValue, Volatility};
 use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
 use sedona_raster::traits::RasterRef;
@@ -41,7 +41,7 @@ fn check_band_agreement<T: PartialEq + std::fmt::Debug>(
     if raster.num_bands() == 0 {
         return exec_err!("{func_name}: raster has no bands");
     }
-    let band0 = raster.band(0).map_err(|e| arrow_datafusion_err!(e))?;
+    let band0 = raster.band(0)?;
     let value = extractor(band0.as_ref());
     for i in 1..raster.num_bands() {
         if let Ok(band) = raster.band(i) {
@@ -170,9 +170,7 @@ impl SedonaScalarKernel for RsNumDimensionsWithBand {
                 // A null or out-of-range band index yields a null result.
                 Some(raster) => match band_index {
                     Some(bi) if bi >= 1 && bi <= raster.num_bands() as i32 => {
-                        let band = raster
-                            .band((bi - 1) as usize)
-                            .map_err(|e| arrow_datafusion_err!(e))?;
+                        let band = raster.band((bi - 1) as usize)?;
                         builder.append_value(band.ndim() as i32);
                         Ok(())
                     }
@@ -281,9 +279,7 @@ impl SedonaScalarKernel for RsDimNamesWithBand {
                 // A null or out-of-range band index yields a null result.
                 Some(raster) => match band_index {
                     Some(bi) if bi >= 1 && bi <= raster.num_bands() as i32 => {
-                        let band = raster
-                            .band((bi - 1) as usize)
-                            .map_err(|e| arrow_datafusion_err!(e))?;
+                        let band = raster.band((bi - 1) as usize)?;
                         for name in band.dim_names() {
                             list_builder.values().append_value(name);
                         }
@@ -424,9 +420,7 @@ impl SedonaScalarKernel for RsDimSizeWithBand {
                 // the band) yields a null result.
                 (Some(raster), Some(name)) => match band_index {
                     Some(bi) if bi >= 1 && bi <= raster.num_bands() as i32 => {
-                        let band = raster
-                            .band((bi - 1) as usize)
-                            .map_err(|e| arrow_datafusion_err!(e))?;
+                        let band = raster.band((bi - 1) as usize)?;
                         match band.dim_size(name) {
                             Some(s) => builder.append_value(s),
                             None => builder.append_null(),
@@ -530,9 +524,7 @@ impl SedonaScalarKernel for RsShapeWithBand {
                 // A null or out-of-range band index yields a null result.
                 Some(raster) => match band_index {
                     Some(bi) if bi >= 1 && bi <= raster.num_bands() as i32 => {
-                        let band = raster
-                            .band((bi - 1) as usize)
-                            .map_err(|e| arrow_datafusion_err!(e))?;
+                        let band = raster.band((bi - 1) as usize)?;
                         for &s in band.shape() {
                             list_builder.values().append_value(s);
                         }
diff --git a/rust/sedona-raster-functions/src/rs_set_band_nodata.rs b/rust/sedona-raster-functions/src/rs_set_band_nodata.rs
index c54c1ce37..4a1e3f42c 100644
--- a/rust/sedona-raster-functions/src/rs_set_band_nodata.rs
+++ b/rust/sedona-raster-functions/src/rs_set_band_nodata.rs
@@ -45,7 +45,7 @@ use arrow_array::types::{Float64Type, Int64Type};
 use arrow_array::Array;
 use arrow_schema::DataType;
 use datafusion_common::error::Result;
-use datafusion_common::{arrow_datafusion_err, exec_err};
+use datafusion_common::exec_err;
 use datafusion_expr::{ColumnarValue, Volatility};
 use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
 use sedona_raster::builder::{RasterBuilder, RasterOverrides};
@@ -121,8 +121,7 @@ impl SedonaScalarKernel for RsSetBandNoDataValue {
 
         let mut builder = RasterBuilder::new(n);
         executor.execute_raster_void(|i, raster_opt| {
-            let null_out =
-                |b: &mut RasterBuilder| b.append_null().map_err(|e| arrow_datafusion_err!(e));
+            let null_out = |b: &mut RasterBuilder| b.append_null().map_err(Into::into);
 
             // A null raster, band, or value yields a null raster.
             let Some(raster) = raster_opt else {
diff --git a/rust/sedona-raster-functions/src/rs_set_georeference.rs b/rust/sedona-raster-functions/src/rs_set_georeference.rs
index 6b0ba72c4..17eaacfbc 100644
--- a/rust/sedona-raster-functions/src/rs_set_georeference.rs
+++ b/rust/sedona-raster-functions/src/rs_set_georeference.rs
@@ -41,7 +41,7 @@ use arrow_array::Array;
 use arrow_schema::DataType;
 use datafusion_common::cast::as_string_array;
 use datafusion_common::error::Result;
-use datafusion_common::{arrow_datafusion_err, exec_datafusion_err, exec_err};
+use datafusion_common::{exec_datafusion_err, exec_err};
 use datafusion_expr::{ColumnarValue, Volatility};
 use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
 use sedona_raster::builder::{RasterBuilder, RasterOverrides};
@@ -112,8 +112,7 @@ impl SedonaScalarKernel for RsSetGeoReference {
 
         let mut builder = RasterBuilder::new(n);
         executor.execute_raster_void(|i, raster_opt| {
-            let null_out =
-                |b: &mut RasterBuilder| b.append_null().map_err(|e| arrow_datafusion_err!(e));
+            let null_out = |b: &mut RasterBuilder| b.append_null().map_err(Into::into);
 
             // A null georef or format is a null *input* and yields a null raster
             // (matching RS_SetCRS); check those first so a null-driven row never
@@ -143,12 +142,10 @@ impl SedonaScalarKernel for RsSetGeoReference {
                         transform: Some(transform),
                     },
                 )
-                .map_err(|e| arrow_datafusion_err!(e))
+                .map_err(Into::into)
         })?;
 
-        executor.finish(Arc::new(
-            builder.finish().map_err(|e| arrow_datafusion_err!(e))?,
-        ))
+        executor.finish(Arc::new(builder.finish()?))
     }
 }
 
diff --git a/rust/sedona-raster-functions/src/rs_slice.rs b/rust/sedona-raster-functions/src/rs_slice.rs
index 571b0c3f6..6ead2ed24 100644
--- a/rust/sedona-raster-functions/src/rs_slice.rs
+++ b/rust/sedona-raster-functions/src/rs_slice.rs
@@ -20,7 +20,7 @@ use std::sync::Arc;
 use arrow_schema::DataType;
 use datafusion_common::cast::{as_int64_array, as_string_array};
 use datafusion_common::error::Result;
-use datafusion_common::{arrow_datafusion_err, exec_err};
+use datafusion_common::exec_err;
 use datafusion_expr::{ColumnarValue, Volatility};
 use sedona_common::sedona_internal_datafusion_err;
 use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
@@ -107,9 +107,7 @@ impl SedonaScalarKernel for RsSlice {
                     require_any_band_has_dim(raster, name, "RS_Slice")?;
 
                     for band_idx in 0..raster.num_bands() {
-                        let band = raster
-                            .band(band_idx)
-                            .map_err(|e| arrow_datafusion_err!(e))?;
+                        let band = raster.band(band_idx)?;
 
                         // Pass-through: bands that don't carry the named
                         // dimension are emitted unchanged. Same convention as
@@ -268,9 +266,7 @@ impl SedonaScalarKernel for RsSliceRange {
                     require_any_band_has_dim(raster, name, "RS_SliceRange")?;
 
                     for band_idx in 0..raster.num_bands() {
-                        let band = raster
-                            .band(band_idx)
-                            .map_err(|e| arrow_datafusion_err!(e))?;
+                        let band = raster.band(band_idx)?;
 
                         // Pass-through: bands that don't carry the named
                         // dimension are emitted unchanged. Same convention as
diff --git a/rust/sedona-raster-functions/src/rs_spatial_predicates.rs b/rust/sedona-raster-functions/src/rs_spatial_predicates.rs
index a7b02d879..a10a9b82b 100644
--- a/rust/sedona-raster-functions/src/rs_spatial_predicates.rs
+++ b/rust/sedona-raster-functions/src/rs_spatial_predicates.rs
@@ -39,12 +39,12 @@ use crate::footprint::write_convexhull_wkb;
 use arrow_array::builder::BooleanBuilder;
 use arrow_schema::DataType;
 use datafusion_common::config::ConfigOptions;
-use datafusion_common::exec_datafusion_err;
 use datafusion_common::exec_err;
 use datafusion_common::Result;
 use datafusion_expr::{ColumnarValue, Volatility};
 use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
 use sedona_geometry::transform::CrsEngine;
+use sedona_raster::error::RasterResultExt;
 use sedona_raster::traits::RasterRef;
 use sedona_schema::crs::{lnglat, CrsRef};
 use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
@@ -378,10 +378,10 @@ fn evaluate_predicate_with_crs<Op: tg::BinaryPredicate>(
 
 /// Evaluate a spatial predicate between two WKB geometries
 fn evaluate_predicate<Op: tg::BinaryPredicate>(wkb_a: &[u8], wkb_b: &[u8]) -> Result<bool> {
-    let geom_a = tg::Geom::parse_wkb(wkb_a, tg::IndexType::Default)
-        .map_err(|e| exec_datafusion_err!("Failed to parse WKB A: {e}"))?;
-    let geom_b = tg::Geom::parse_wkb(wkb_b, tg::IndexType::Default)
-        .map_err(|e| exec_datafusion_err!("Failed to parse WKB B: {e}"))?;
+    let geom_a =
+        tg::Geom::parse_wkb(wkb_a, tg::IndexType::Default).context("Failed to parse WKB A")?;
+    let geom_b =
+        tg::Geom::parse_wkb(wkb_b, tg::IndexType::Default).context("Failed to parse WKB B")?;
 
     Ok(Op::evaluate(&geom_a, &geom_b))
 }
diff --git a/rust/sedona-raster-functions/src/rs_value.rs b/rust/sedona-raster-functions/src/rs_value.rs
index 3b38d04ab..53157412c 100644
--- a/rust/sedona-raster-functions/src/rs_value.rs
+++ b/rust/sedona-raster-functions/src/rs_value.rs
@@ -38,12 +38,13 @@ use arrow_array::{builder::Float64Builder, Array, ArrayRef, Float64Array, Struct
 use arrow_schema::DataType;
 use datafusion_common::cast::as_int32_array;
 use datafusion_common::config::ConfigOptions;
-use datafusion_common::{exec_datafusion_err, exec_err, Result, ScalarValue};
+use datafusion_common::{exec_err, Result, ScalarValue};
 use datafusion_expr::{ColumnarValue, Volatility};
 use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
 use sedona_geometry::transform::CrsTransform;
 use sedona_geometry::wkb_header::read_point_xy;
 use sedona_raster::array::RasterStructArray;
+use sedona_raster::error::RasterResultExt;
 use sedona_raster::traits::RasterRef;
 use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
 
@@ -332,12 +333,8 @@ impl RsValuePoint {
                         "RS_Value supports 2-D rasters only; band is not a 2-D (y, x) grid"
                     );
                 }
-                let buffer = band
-                    .nd_buffer()
-                    .map_err(|e| exec_datafusion_err!("RS_Value: {e}"))?;
-                let nodata = band
-                    .nodata_as_f64()
-                    .map_err(|e| exec_datafusion_err!("RS_Value: {e}"))?;
+                let buffer = band.nd_buffer().context("RS_Value")?;
+                let nodata = band.nodata_as_f64().context("RS_Value")?;
                 for (i, x, y, _band) in selection {
                     if let Some((col, row)) = xy_to_pixel("RS_Value", transform, x, y)? {
                         out[i] = read_pixel("RS_Value", &buffer, nodata, col, row)?;
@@ -368,15 +365,11 @@ fn resolve_point_xy(
     point_wkb: &[u8],
     trans: Option<&dyn CrsTransform>,
 ) -> Result<Option<(f64, f64)>> {
-    let Some(mut xy) =
-        read_point_xy(point_wkb).map_err(|e| exec_datafusion_err!("RS_Value: {e}"))?
-    else {
+    let Some(mut xy) = read_point_xy(point_wkb).context("RS_Value")? else {
         return Ok(None);
     };
     if let Some(trans) = trans {
-        trans
-            .transform_coord(&mut xy)
-            .map_err(|e| exec_datafusion_err!("RS_Value: {e}"))?;
+        trans.transform_coord(&mut xy).context("RS_Value")?;
     }
     Ok(Some(xy))
 }
@@ -401,12 +394,8 @@ fn sample_pixel(
     if !band.is_spatial_2d() {
         return exec_err!("RS_Value supports 2-D rasters only; band is not a 2-D (y, x) grid");
     }
-    let buffer = band
-        .nd_buffer()
-        .map_err(|e| exec_datafusion_err!("RS_Value: {e}"))?;
-    let nodata = band
-        .nodata_as_f64()
-        .map_err(|e| exec_datafusion_err!("RS_Value: {e}"))?;
+    let buffer = band.nd_buffer().context("RS_Value")?;
+    let nodata = band.nodata_as_f64().context("RS_Value")?;
     read_pixel("RS_Value", &buffer, nodata, col, row)
 }
 
diff --git a/rust/sedona-raster-functions/src/rs_values.rs b/rust/sedona-raster-functions/src/rs_values.rs
index ed839ffe3..a8cd92210 100644
--- a/rust/sedona-raster-functions/src/rs_values.rs
+++ b/rust/sedona-raster-functions/src/rs_values.rs
@@ -44,10 +44,11 @@ use arrow_array::{Array, ArrayRef, StructArray};
 use arrow_schema::DataType;
 use datafusion_common::cast::as_int32_array;
 use datafusion_common::config::ConfigOptions;
-use datafusion_common::{exec_datafusion_err, exec_err, Result, ScalarValue};
+use datafusion_common::{exec_err, Result, ScalarValue};
 use datafusion_expr::{ColumnarValue, Volatility};
 use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
 use sedona_raster::array::RasterStructArray;
+use sedona_raster::error::RasterResultExt;
 use sedona_raster::traits::{BandRef, NdBuffer, RasterRef};
 use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
 
@@ -185,12 +186,8 @@ impl RsValues {
                 // this row, then sample every sub-point against them.
                 let raster_crs = resolve_crs(raster.crs())?;
                 let band = resolve_band_2d(raster, band_num)?;
-                let buffer = band
-                    .nd_buffer()
-                    .map_err(|e| exec_datafusion_err!("RS_Values: {e}"))?;
-                let nodata = band
-                    .nodata_as_f64()
-                    .map_err(|e| exec_datafusion_err!("RS_Values: {e}"))?;
+                let buffer = band.nd_buffer().context("RS_Values")?;
+                let nodata = band.nodata_as_f64().context("RS_Values")?;
                 let transform = raster.transform();
 
                 // Sample each sub-point in one pass: the visitor transforms
@@ -353,12 +350,8 @@ impl RsValues {
         match const_band {
             Some(band_num) => {
                 let band = resolve_band_2d(&raster, band_num)?;
-                let buffer = band
-                    .nd_buffer()
-                    .map_err(|e| exec_datafusion_err!("RS_Values: {e}"))?;
-                let nodata = band
-                    .nodata_as_f64()
-                    .map_err(|e| exec_datafusion_err!("RS_Values: {e}"))?;
+                let buffer = band.nd_buffer().context("RS_Values")?;
+                let nodata = band.nodata_as_f64().context("RS_Values")?;
                 for row in &rows {
                     let Some((start, len, _band)) = row else {
                         list_builder.append_null();
@@ -377,12 +370,8 @@ impl RsValues {
                         continue;
                     };
                     let band = resolve_band_2d(&raster, *band_num)?;
-                    let buffer = band
-                        .nd_buffer()
-                        .map_err(|e| exec_datafusion_err!("RS_Values: {e}"))?;
-                    let nodata = band
-                        .nodata_as_f64()
-                        .map_err(|e| exec_datafusion_err!("RS_Values: {e}"))?;
+                    let buffer = band.nd_buffer().context("RS_Values")?;
+                    let nodata = band.nodata_as_f64().context("RS_Values")?;
                     for xy in &coords[*start..*start + *len] {
                         append_sample(*xy, transform, &buffer, nodata, &mut list_builder)?;
                     }
diff --git a/rust/sedona-raster-functions/src/sampling.rs b/rust/sedona-raster-functions/src/sampling.rs
index 0cca0522d..144c126f7 100644
--- a/rust/sedona-raster-functions/src/sampling.rs
+++ b/rust/sedona-raster-functions/src/sampling.rs
@@ -33,6 +33,7 @@ use datafusion_common::{exec_datafusion_err, exec_err, DataFusionError, Result};
 use datafusion_expr::ColumnarValue;
 use sedona_geometry::error::SedonaGeometryError;
 use sedona_geometry::transform::{visit_point_coords, CrsEngine, CrsTransform};
+use sedona_raster::error::RasterResultExt;
 use sedona_raster::geo_transform::GeoTransformEx;
 use sedona_raster::traits::{nodata_bytes_to_f64_lossless, BandRef, NdBuffer, RasterRef};
 use sedona_schema::crs::CrsRef;
@@ -180,7 +181,7 @@ pub(crate) fn visit_points(
     trans: Option<&dyn CrsTransform>,
     mut visit: impl FnMut(Option<(f64, f64)>) -> Result<()>,
 ) -> Result<()> {
-    let geom = read_wkb(wkb).map_err(|e| exec_datafusion_err!("{func}: {e}"))?;
+    let geom = read_wkb(wkb).context(func)?;
     visit_point_coords(&geom, trans, |xy| {
         visit(xy).map_err(|e| SedonaGeometryError::External(Box::new(e)))
     })
@@ -269,8 +270,7 @@ pub(crate) fn read_pixel(
     // silently rounding) on Int64/UInt64 values beyond f64's exact-integer
     // range (2^53) — the value functions return a Double, so such a pixel can't
     // be represented faithfully; failing loudly is preferred over a wrong value.
-    let value = nodata_bytes_to_f64_lossless(bytes, &buffer.data_type)
-        .map_err(|e| exec_datafusion_err!("{func}: {e}"))?;
+    let value = nodata_bytes_to_f64_lossless(bytes, &buffer.data_type).context(func)?;
 
     if let Some(nodata) = nodata {
         if value == nodata || (value.is_nan() && nodata.is_nan()) {
diff --git a/rust/sedona-raster-gdal/src/gdal_common.rs b/rust/sedona-raster-gdal/src/gdal_common.rs
index f103e0142..6d3b3760f 100644
--- a/rust/sedona-raster-gdal/src/gdal_common.rs
+++ b/rust/sedona-raster-gdal/src/gdal_common.rs
@@ -28,9 +28,7 @@ use sedona_raster::geo_transform::GeoTransform;
 use sedona_raster::traits::{is_spatial_dim_pair, RasterRef};
 use sedona_schema::raster::BandDataType;
 
-use datafusion_common::{
-    arrow_datafusion_err, exec_datafusion_err, exec_err, DataFusionError, Result,
-};
+use datafusion_common::{exec_datafusion_err, exec_err, DataFusionError, Result};
 
 /// Execute a closure with a reference to the global [`Gdal`] handle,
 /// converting initialization errors to [`DataFusionError`].
@@ -173,8 +171,8 @@ pub unsafe fn raster_ref_to_gdal_mem<R: RasterRef + ?Sized>(
     raster: &R,
     band_indices: &[usize],
 ) -> Result<Dataset> {
-    let width = raster.width().map_err(|e| arrow_datafusion_err!(e))? as usize;
-    let height = raster.height().map_err(|e| arrow_datafusion_err!(e))? as usize;
+    let width = raster.width()? as usize;
+    let height = raster.height()? as usize;
 
     // The N-D → flat-GDAL plane layout (band-major, plane-major). Deriving it
     // performs the trailing-(y, x)-pair check and records each band's plane
@@ -190,9 +188,7 @@ pub unsafe fn raster_ref_to_gdal_mem<R: RasterRef + ?Sized>(
     let mut base_ptrs: Vec<*mut u8> = Vec::with_capacity(layout.bands.len());
     for (&src_band_index, plan) in band_indices.iter().zip(&layout.bands) {
         // `band_indices` are 1-based; the `band` accessor is 0-based.
-        let band = raster
-            .band(src_band_index - 1)
-            .map_err(|e| arrow_datafusion_err!(e))?;
+        let band = raster.band(src_band_index - 1)?;
 
         // The plane's 2-D extent must equal the MEM dataset's (and the raster's
         // spatial grid); otherwise the per-plane byte slicing would disagree
@@ -345,7 +341,7 @@ impl GdalBandLayout {
         let mut plans = Vec::with_capacity(band_indices.len());
         for &i in band_indices {
             // `band_indices` are 1-based; the `band` accessor is 0-based.
-            let band = raster.band(i - 1).map_err(|e| arrow_datafusion_err!(e))?;
+            let band = raster.band(i - 1)?;
             let dim_names: Vec<String> = band.dim_names().iter().map(|s| s.to_string()).collect();
             let ndim = dim_names.len();
             if ndim < 2 || !is_spatial_dim_pair(&dim_names[ndim - 2], &dim_names[ndim - 1]) {
diff --git a/rust/sedona-raster-gdal/src/gdal_dataset_provider.rs b/rust/sedona-raster-gdal/src/gdal_dataset_provider.rs
index 97798c8c2..8740178d3 100644
--- a/rust/sedona-raster-gdal/src/gdal_dataset_provider.rs
+++ b/rust/sedona-raster-gdal/src/gdal_dataset_provider.rs
@@ -19,13 +19,12 @@ use std::convert::TryInto;
 use std::{cell::RefCell, marker::PhantomData, num::NonZeroUsize, rc::Rc};
 
 use datafusion_common::config::ConfigOptions;
-use datafusion_common::{
-    arrow_datafusion_err, exec_datafusion_err, exec_err, DataFusionError, Result,
-};
+use datafusion_common::{exec_datafusion_err, exec_err, DataFusionError, Result};
 
 use sedona_gdal::dataset::Dataset;
 use sedona_gdal::gdal::Gdal;
 use sedona_gdal::raster::types::GdalDataType;
+use sedona_raster::error::RasterResultExt;
 use sedona_raster::geo_transform::{GeoTransform, GeoTransformEx};
 
 use sedona_common::SedonaOptions;
@@ -214,8 +213,8 @@ impl GDALDatasetCache {
     ) -> Result<(Rc<Dataset>, Vec<Rc<Dataset>>)> {
         let num_bands = raster.num_bands();
 
-        let metadata_width = raster.width().map_err(|e| arrow_datafusion_err!(e))?;
-        let metadata_height = raster.height().map_err(|e| arrow_datafusion_err!(e))?;
+        let metadata_width = raster.width()?;
+        let metadata_height = raster.height()?;
         let width: i32 = metadata_width.try_into().map_err(|_| {
             exec_datafusion_err!(
                 "Raster width {} exceeds supported GDAL/i32 limit {}",
@@ -259,7 +258,7 @@ impl GDALDatasetCache {
         // `i` is 1-based here because it also indexes GDAL's own 1-based
         // `rasterband(i)` below; the raster's `band` accessor is 0-based.
         for i in 1..=num_bands {
-            let band = raster.band(i - 1).map_err(|e| arrow_datafusion_err!(e))?;
+            let band = raster.band(i - 1)?;
 
             if !band.is_spatial_2d() {
                 return exec_err!(
@@ -306,8 +305,7 @@ impl GDALDatasetCache {
                 let uri = band.outdb_uri().ok_or_else(|| {
                     exec_datafusion_err!("Band {} is out-db but missing outdb_uri", i)
                 })?;
-                let (url, source_band_num_u32) =
-                    split_outdb_band_fragment(uri).map_err(|e| arrow_datafusion_err!(e))?;
+                let (url, source_band_num_u32) = split_outdb_band_fragment(uri)?;
                 let source_band_num: usize = source_band_num_u32
                     .try_into()
                     .map_err(|_| exec_datafusion_err!("Band {} out-db band_id is too large", i))?;
@@ -386,7 +384,7 @@ impl<'a> GDALDatasetProvider<'a> {
         let mut indb_band_indices = Vec::with_capacity(num_bands);
         let mut has_outdb = false;
         for i in 1..=num_bands {
-            let band = raster.band(i - 1).map_err(|e| arrow_datafusion_err!(e))?;
+            let band = raster.band(i - 1)?;
             if band.is_indb() {
                 indb_band_indices.push(i);
             } else {
@@ -482,7 +480,7 @@ impl VrtKey {
 
         let mut band_keys = Vec::with_capacity(num_bands);
         for i in 0..num_bands {
-            let band = raster.band(i).map_err(|e| arrow_datafusion_err!(e))?;
+            let band = raster.band(i)?;
             let band_type = band.data_type();
             let nodata_bits = match (band.nodata(), band_type) {
                 (Some(bytes), BandDataType::UInt64) => {
@@ -504,8 +502,7 @@ impl VrtKey {
             // URI; in-db bands have no URI hint here.
             let (outdb_url, outdb_band_id) = match band.outdb_uri() {
                 Some(uri) => {
-                    let (url, band_num) =
-                        split_outdb_band_fragment(uri).map_err(|e| arrow_datafusion_err!(e))?;
+                    let (url, band_num) = split_outdb_band_fragment(uri)?;
                     (Some(normalize_outdb_source_path(&url)), Some(band_num))
                 }
                 None => (None, None),
@@ -566,7 +563,7 @@ fn compute_vrt_simple_source_windows(
     // Compute the pixel/line offset of the destination upper-left in the source grid.
     let inv_src = src_gt
         .invert()
-        .map_err(|e| exec_datafusion_err!("Failed to invert source geotransform: {e}"))?;
+        .context("Failed to invert source geotransform")?;
     let (off_x_f, off_y_f) = inv_src.apply(dst_gt[0], dst_gt[3]);
 
     let off_x_r: f64 = off_x_f.round();
diff --git a/rust/sedona-raster-gdal/src/mask.rs b/rust/sedona-raster-gdal/src/mask.rs
index 3cc0aaea7..38de9867e 100644
--- a/rust/sedona-raster-gdal/src/mask.rs
+++ b/rust/sedona-raster-gdal/src/mask.rs
@@ -26,11 +26,12 @@
 //! module owns only the window addressing and rasterization, not the
 //! per-pixel consumption.
 
-use datafusion_common::{exec_datafusion_err, Result};
+use datafusion_common::Result;
 use sedona_gdal::gdal::Gdal;
 use sedona_gdal::mem::MemDatasetBuilder;
 use sedona_gdal::raster::types::GdalDataType;
 use sedona_gdal::vector::geometry::Geometry;
+use sedona_raster::error::RasterResultExt;
 use sedona_raster::geo_transform::{GeoTransform, GeoTransformEx};
 
 /// A rectangular pixel window (offset + size) into a raster grid.
@@ -62,7 +63,7 @@ pub fn envelope_window(
     let env = geometry.envelope();
     let inverse = transform
         .invert()
-        .map_err(|e| exec_datafusion_err!("raster mask: geotransform is not invertible: {e}"))?;
+        .context("raster mask: geotransform is not invertible")?;
 
     let corners = [
         (env.MinX, env.MinY),
@@ -128,7 +129,7 @@ pub fn rasterize_geometry_mask(
 ) -> Result<()> {
     let mask_dataset =
         MemDatasetBuilder::create(gdal, window.width, window.height, 1, GdalDataType::UInt8)
-            .map_err(|e| exec_datafusion_err!("raster mask: failed to create mask dataset: {e}"))?;
+            .context("raster mask: failed to create mask dataset")?;
     let (window_ulx, window_uly) = transform.apply(window.col_off as f64, window.row_off as f64);
     let mask_transform = [
         window_ulx,
@@ -140,14 +141,14 @@ pub fn rasterize_geometry_mask(
     ];
     mask_dataset
         .set_geo_transform(&mask_transform)
-        .map_err(|e| exec_datafusion_err!("raster mask: failed to set mask geotransform: {e}"))?;
+        .context("raster mask: failed to set mask geotransform")?;
 
     gdal.rasterize_affine(&mask_dataset, &[1], &[geometry], &[1.0], all_touched)
-        .map_err(|e| exec_datafusion_err!("raster mask: failed to rasterize geometry: {e}"))?;
+        .context("raster mask: failed to rasterize geometry")?;
 
     let mask_band = mask_dataset
         .rasterband(1)
-        .map_err(|e| exec_datafusion_err!("raster mask: failed to read mask band: {e}"))?;
+        .context("raster mask: failed to read mask band")?;
     let mask_buffer = mask_band
         .read_as::<u8>(
             (0, 0),
@@ -155,7 +156,7 @@ pub fn rasterize_geometry_mask(
             (window.width, window.height),
             None,
         )
-        .map_err(|e| exec_datafusion_err!("raster mask: failed to read mask: {e}"))?;
+        .context("raster mask: failed to read mask")?;
     out.clear();
     out.extend_from_slice(mask_buffer.data());
     Ok(())
diff --git a/rust/sedona-raster-gdal/src/rs_as_geotiff.rs b/rust/sedona-raster-gdal/src/rs_as_geotiff.rs
index b00626127..0556afcf5 100644
--- a/rust/sedona-raster-gdal/src/rs_as_geotiff.rs
+++ b/rust/sedona-raster-gdal/src/rs_as_geotiff.rs
@@ -40,6 +40,7 @@ use datafusion_expr::{ColumnarValue, Volatility};
 use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
 use sedona_gdal::vsi::VSIBuffer;
 use sedona_raster::array::RasterRefImpl;
+use sedona_raster::error::RasterResultExt;
 use sedona_raster::traits::RasterRef;
 use sedona_raster_functions::RasterExecutor;
 use sedona_schema::datatypes::SedonaType;
@@ -150,12 +151,12 @@ impl RsAsGeoTiff {
     ) -> Result<VSIBuffer> {
         let raster_ds = provider
             .raster_ref_to_gdal(raster)
-            .map_err(|e| exec_datafusion_err!("Failed to create GDAL dataset: {}", e))?;
+            .context("Failed to create GDAL dataset")?;
         let source_dataset = raster_ds.as_dataset();
 
         let driver = gdal
             .get_driver_by_name("GTiff")
-            .map_err(|e| exec_datafusion_err!("Failed to get GTiff driver: {}", e))?;
+            .context("Failed to get GTiff driver")?;
 
         // Validate and map the quality up front so an out-of-range value errors
         // for every codec, not only JPEG (the codecs that ignore quality should
@@ -211,7 +212,7 @@ impl RsAsGeoTiff {
         // the bytes to the vsimem file.
         source_dataset
             .create_copy(&driver, &vsi_path, &options_refs)
-            .map_err(|e| exec_datafusion_err!("Failed to create GeoTiff: {}", e))?;
+            .context("Failed to create GeoTiff")?;
 
         // Seize the vsimem file's buffer without copying: `VSIBuffer` owns the
         // GDAL allocation (freed on drop) and unlinks the file, so the only
@@ -220,7 +221,7 @@ impl RsAsGeoTiff {
         // `create_copy` or the seize fails.
         let bytes = gdal
             .get_vsi_mem_file_buffer_owned(&vsi_path)
-            .map_err(|e| exec_datafusion_err!("Failed to read GeoTiff bytes: {}", e))?;
+            .context("Failed to read GeoTiff bytes")?;
 
         drop(guard);
         Ok(bytes)
@@ -297,9 +298,7 @@ fn predictor_for(raster: &RasterRefImpl) -> Result<i32> {
     if raster.num_bands() == 0 {
         return Ok(2);
     }
-    let band = raster
-        .band(0)
-        .map_err(|e| exec_datafusion_err!("RS_AsGeoTiff: {e}"))?;
+    let band = raster.band(0).context("RS_AsGeoTiff")?;
     let data_type = band.data_type();
     Ok(match data_type {
         BandDataType::Float32 | BandDataType::Float64 => 3,
@@ -449,8 +448,7 @@ impl SedonaScalarKernel for RsAsGeoTiff {
 
         with_gdal(|gdal| {
             configure_thread_local_options(gdal, config_options)?;
-            let provider = thread_local_provider(gdal)
-                .map_err(|e| exec_datafusion_err!("Failed to init GDAL provider: {e}"))?;
+            let provider = thread_local_provider(gdal).context("Failed to init GDAL provider")?;
             executor.execute_raster_void(|_i, raster_opt| {
                 let compression_opt = compression_iter.next().unwrap();
                 let quality_opt = quality_iter.next().unwrap();
diff --git a/rust/sedona-raster-gdal/src/rs_as_raster.rs b/rust/sedona-raster-gdal/src/rs_as_raster.rs
index 01ac1b076..349c33a10 100644
--- a/rust/sedona-raster-gdal/src/rs_as_raster.rs
+++ b/rust/sedona-raster-gdal/src/rs_as_raster.rs
@@ -37,6 +37,7 @@ use sedona_gdal::mem::MemDatasetBuilder;
 use sedona_gdal::raster::{rasterband::RasterBand, types::Buffer};
 use sedona_raster::array::RasterRefImpl;
 use sedona_raster::builder::RasterBuilder;
+use sedona_raster::error::RasterResultExt;
 use sedona_raster::traits::RasterRef;
 use sedona_raster_functions::{
     crs_utils::{align_wkb_to_crs, resolve_crs},
@@ -242,24 +243,22 @@ impl SedonaScalarKernel for RsAsRaster {
                     nodata_value_opt,
                     use_geometry_extent_opt,
                 )
-                .map_err(|e| exec_datafusion_err!("RS_AsRaster failed: {}", e))?;
+                .context("RS_AsRaster failed")?;
 
                 rasterized
                     .grid
                     .start_raster_into(&mut builder, raster.crs())
-                    .map_err(|e| exec_datafusion_err!("Failed to start output raster: {}", e))?;
+                    .context("Failed to start output raster")?;
                 builder
                     .start_band_2d(rasterized.data_type, rasterized.nodata.as_deref())
-                    .map_err(|e| {
-                        exec_datafusion_err!("Failed to start output raster band: {}", e)
-                    })?;
+                    .context("Failed to start output raster band")?;
                 builder.band_data_writer().append_value(rasterized.data);
-                builder.finish_band().map_err(|e| {
-                    exec_datafusion_err!("Failed to finish output raster band: {}", e)
-                })?;
+                builder
+                    .finish_band()
+                    .context("Failed to finish output raster band")?;
                 builder
                     .finish_raster()
-                    .map_err(|e| exec_datafusion_err!("Failed to finish output raster: {}", e))?;
+                    .context("Failed to finish output raster")?;
 
                 Ok(())
             })
@@ -436,7 +435,7 @@ fn as_raster(
 
     let geometry = gdal
         .geometry_from_wkb(geom_wkb)
-        .map_err(|e| exec_datafusion_err!("Failed to parse geometry from WKB: {}", e))?;
+        .context("Failed to parse geometry from WKB")?;
 
     let (out_width, out_height, out_ulx, out_uly) = if use_geometry_extent {
         let env = geometry.envelope();
@@ -491,7 +490,7 @@ fn as_raster(
         1,
         band_data_type_to_gdal(&band_type),
     )
-    .map_err(|e| exec_datafusion_err!("Failed to create dataset: {}", e))?;
+    .context("Failed to create dataset")?;
 
     let out_transform = [
         out_ulx,
@@ -503,17 +502,16 @@ fn as_raster(
     ];
     out_dataset
         .set_geo_transform(&out_transform)
-        .map_err(|e| exec_datafusion_err!("Failed to set geotransform: {}", e))?;
+        .context("Failed to set geotransform")?;
 
-    let provider = thread_local_provider(gdal)
-        .map_err(|e| exec_datafusion_err!("Failed to init GDAL provider: {}", e))?;
+    let provider = thread_local_provider(gdal).context("Failed to init GDAL provider")?;
     let ref_raster_ds = provider
         .raster_ref_to_gdal(reference_raster)
-        .map_err(|e| exec_datafusion_err!("Failed to create GDAL dataset: {}", e))?;
+        .context("Failed to create GDAL dataset")?;
     if let Ok(srs) = ref_raster_ds.as_dataset().spatial_ref() {
         out_dataset
             .set_spatial_ref(&srs)
-            .map_err(|e| exec_datafusion_err!("Failed to set spatial reference: {}", e))?;
+            .context("Failed to set spatial reference")?;
     }
 
     let init_value =
@@ -524,16 +522,16 @@ fn as_raster(
         let nodata = cast_f64_to_band_value(nodata, band_type, "nodata value")?;
         let band = out_dataset
             .rasterband(1)
-            .map_err(|e| exec_datafusion_err!("Failed to get output band: {}", e))?;
+            .context("Failed to get output band")?;
         set_band_nodata(&band, nodata)?;
     }
 
     gdal.rasterize_affine(&out_dataset, &[1], &[geometry], &[burn_value], all_touched)
-        .map_err(|e| exec_datafusion_err!("Failed to rasterize geometry: {}", e))?;
+        .context("Failed to rasterize geometry")?;
 
     let band = out_dataset
         .rasterband(1)
-        .map_err(|e| exec_datafusion_err!("Failed to get output band: {}", e))?;
+        .context("Failed to get output band")?;
     let band_bytes = band
         .read_as_bytes(
             (0, 0),
@@ -541,7 +539,7 @@ fn as_raster(
             (out_width, out_height),
             None,
         )
-        .map_err(|e| exec_datafusion_err!("Failed to read band data: {}", e))?;
+        .context("Failed to read band data")?;
 
     let out_grid = Grid {
         transform: out_transform,
@@ -621,12 +619,10 @@ fn initialize_band_t<T: sedona_gdal::raster::types::GdalType + Copy>(
     height: usize,
     init_value: T,
 ) -> Result<()> {
-    let band = dataset
-        .rasterband(1)
-        .map_err(|e| exec_datafusion_err!("Failed to get output band: {}", e))?;
+    let band = dataset.rasterband(1).context("Failed to get output band")?;
     let mut buffer = Buffer::new((width, height), vec![init_value; width * height]);
     band.write((0, 0), (width, height), &mut buffer)
-        .map_err(|e| exec_datafusion_err!("Failed to initialize band: {}", e))?;
+        .context("Failed to initialize band")?;
     Ok(())
 }
 
diff --git a/rust/sedona-raster-gdal/src/rs_clip.rs b/rust/sedona-raster-gdal/src/rs_clip.rs
index 96d38ce86..4ec48fdf9 100644
--- a/rust/sedona-raster-gdal/src/rs_clip.rs
+++ b/rust/sedona-raster-gdal/src/rs_clip.rs
@@ -29,7 +29,7 @@ use datafusion_common::cast::{as_boolean_array, as_float64_array, as_int32_array
 use datafusion_common::config::ConfigOptions;
 use datafusion_common::error::Result;
 use datafusion_common::exec_err;
-use datafusion_common::{exec_datafusion_err, ScalarValue};
+use datafusion_common::ScalarValue;
 use datafusion_expr::{ColumnarValue, Volatility};
 use sedona_common::sedona_internal_err;
 use sedona_gdal::gdal::Gdal;
@@ -38,6 +38,7 @@ use arrow_schema::DataType;
 use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
 use sedona_raster::array::RasterRefImpl;
 use sedona_raster::builder::RasterBuilder;
+use sedona_raster::error::RasterResultExt;
 use sedona_raster::traits::{is_spatial_dim_pair, RasterRef};
 use sedona_raster_functions::crs_utils::{crs_transform_wkb, resolve_crs, with_crs_engine};
 use sedona_raster_functions::rs_ensure_loaded::{
@@ -383,7 +384,7 @@ fn clip_raster(
     // Parse geometry from WKB
     let geometry = gdal
         .geometry_from_wkb(geom_wkb)
-        .map_err(|e| exec_datafusion_err!("Failed to parse geometry from WKB: {}", e))?;
+        .context("Failed to parse geometry from WKB")?;
 
     // GDAL geotransform: [upper_left_x, scale_x, skew_x, upper_left_y, skew_y, scale_y].
     let geotransform = raster_geo_transform(raster)?;
@@ -439,7 +440,7 @@ fn clip_raster(
         // `band_idx` is 1-based; the `band`/`band_name` accessors are 0-based.
         let band = raster
             .band(band_idx - 1)
-            .map_err(|e| exec_datafusion_err!("Failed to get band {}: {}", band_idx, e))?;
+            .with_context(|| format!("Failed to get band {band_idx}"))?;
         let band_name = raster.band_name(band_idx - 1).map(|s| s.to_string());
 
         let data_type = band.data_type();
@@ -483,12 +484,12 @@ fn clip_raster(
         // `as_contiguous` borrows the band bytes; we only ever read them here
         // (the mask/crop helpers write into the band's output buffer), so no
         // copy is needed.
-        let nd_buffer = band.nd_buffer().map_err(|e| {
-            exec_datafusion_err!("RS_Clip: failed to read band {}: {}", band_idx, e)
-        })?;
-        let original_data = nd_buffer.as_contiguous().map_err(|e| {
-            exec_datafusion_err!("RS_Clip: band {} is not contiguous: {}", band_idx, e)
-        })?;
+        let nd_buffer = band
+            .nd_buffer()
+            .with_context(|| format!("RS_Clip: failed to read band {band_idx}"))?;
+        let original_data = nd_buffer
+            .as_contiguous()
+            .with_context(|| format!("RS_Clip: band {band_idx} is not contiguous"))?;
 
         // nodata precedence: the explicit argument, then the band's own nodata
         // bytes (used verbatim — no lossy f64 round-trip for Int64/UInt64),
@@ -498,9 +499,8 @@ fn clip_raster(
         // rather than silently saturating — e.g. -9999 on UInt8 would collide
         // with real zero-adjacent data.
         let nodata_bytes: Vec<u8> = match custom_nodata {
-            Some(cn) => nodata_f64_to_bytes(cn, &data_type).map_err(|e| {
-                exec_datafusion_err!("RS_Clip: invalid no_data_value for band {band_idx}: {e}")
-            })?,
+            Some(cn) => nodata_f64_to_bytes(cn, &data_type)
+                .with_context(|| format!("RS_Clip: invalid no_data_value for band {band_idx}"))?,
             None => match band.nodata() {
                 Some(bytes) => bytes.to_vec(),
                 None => data_type.min_value_le_bytes(),
@@ -722,7 +722,7 @@ fn build_clipped_raster(
             &spatial_shape,
             original_raster.crs(),
         )
-        .map_err(|e| exec_datafusion_err!("Failed to start raster: {}", e))?;
+        .context("Failed to start raster")?;
 
     for band in clipped_data.bands {
         let dim_names: Vec<&str> = band.dim_names.iter().map(String::as_str).collect();
@@ -739,9 +739,7 @@ fn build_clipped_raster(
         )?;
     }
 
-    builder
-        .finish_raster()
-        .map_err(|e| exec_datafusion_err!("Failed to finish raster: {}", e))?;
+    builder.finish_raster().context("Failed to finish raster")?;
 
     Ok(())
 }
diff --git a/rust/sedona-raster-gdal/src/rs_from_gdal_raster.rs b/rust/sedona-raster-gdal/src/rs_from_gdal_raster.rs
index 281692dce..fec0598a3 100644
--- a/rust/sedona-raster-gdal/src/rs_from_gdal_raster.rs
+++ b/rust/sedona-raster-gdal/src/rs_from_gdal_raster.rs
@@ -27,7 +27,7 @@ use arrow_array::{cast::AsArray, Array};
 use arrow_schema::DataType;
 use datafusion_common::config::ConfigOptions;
 use datafusion_common::error::Result;
-use datafusion_common::{exec_datafusion_err, ScalarValue};
+use datafusion_common::ScalarValue;
 use datafusion_expr::{ColumnarValue, Volatility};
 use sedona_common::sedona_internal_err;
 use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
@@ -35,6 +35,7 @@ use sedona_gdal::gdal::Gdal;
 use sedona_gdal::gdal_dyn_bindgen::{GDAL_OF_RASTER, GDAL_OF_READONLY};
 use sedona_gdal::raster::types::DatasetOptions;
 use sedona_raster::builder::RasterBuilder;
+use sedona_raster::error::RasterResultExt;
 use sedona_raster_functions::rs_ensure_loaded::RETURNS_BYTES_METADATA_KEY;
 use sedona_schema::datatypes::{SedonaType, RASTER};
 use sedona_schema::matchers::ArgMatcher;
@@ -83,7 +84,7 @@ impl RsFromGDALRaster {
     fn append_gdal_raster(gdal: &Gdal, content: &[u8], builder: &mut RasterBuilder) -> Result<()> {
         let vsi_path = Self::generate_vsi_path();
         gdal.create_mem_file(&vsi_path, content)
-            .map_err(|e| exec_datafusion_err!("Failed to create VSI memory file: {e}"))?;
+            .context("Failed to create VSI memory file")?;
 
         // Open + decode, then always unlink the VSI file (the dataset is dropped
         // at the end of the closure, before the unlink).
@@ -113,9 +114,7 @@ impl RsFromGDALRaster {
     ) -> Result<()> {
         for row in rows {
             match row {
-                None => builder
-                    .append_null()
-                    .map_err(|e| exec_datafusion_err!("Failed to append null: {e}"))?,
+                None => builder.append_null().context("Failed to append null")?,
                 Some(content) => Self::append_gdal_raster(gdal, content, builder)?,
             }
         }
@@ -132,9 +131,7 @@ impl RsFromGDALRaster {
     ) -> Result<arrow_array::StructArray> {
         let mut builder = RasterBuilder::new(1);
         Self::append_gdal_raster(gdal, content, &mut builder)?;
-        builder
-            .finish()
-            .map_err(|e| exec_datafusion_err!("Failed to build raster: {e}"))
+        Ok(builder.finish().context("Failed to build raster")?)
     }
 }
 
@@ -166,7 +163,7 @@ impl SedonaScalarKernel for RsFromGDALRaster {
             let content_array = match &args[0] {
                 ColumnarValue::Scalar(scalar) => scalar
                     .to_array()
-                    .map_err(|e| exec_datafusion_err!("Failed to convert scalar to array: {e}"))?,
+                    .context("Failed to convert scalar to array")?,
                 ColumnarValue::Array(array) => array.clone(),
             };
 
@@ -188,9 +185,7 @@ impl SedonaScalarKernel for RsFromGDALRaster {
                     )
                 }
             }
-            let result = builder
-                .finish()
-                .map_err(|e| exec_datafusion_err!("Failed to build raster: {e}"))?;
+            let result = builder.finish().context("Failed to build raster")?;
 
             match &args[0] {
                 ColumnarValue::Scalar(_) => Ok(ColumnarValue::Scalar(ScalarValue::try_from_array(
diff --git a/rust/sedona-raster-gdal/src/rs_metadata.rs b/rust/sedona-raster-gdal/src/rs_metadata.rs
index 6cdd50121..46089b41c 100644
--- a/rust/sedona-raster-gdal/src/rs_metadata.rs
+++ b/rust/sedona-raster-gdal/src/rs_metadata.rs
@@ -25,9 +25,9 @@ use arrow_buffer::NullBufferBuilder;
 use arrow_schema::{DataType, Field, Fields};
 use datafusion_common::config::ConfigOptions;
 use datafusion_common::error::Result;
-use datafusion_common::exec_datafusion_err;
 use datafusion_expr::{ColumnarValue, Volatility};
 use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
+use sedona_raster::error::RasterResultExt;
 use sedona_raster::traits::RasterRef;
 use sedona_schema::crs::deserialize_crs;
 use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
@@ -108,8 +108,7 @@ impl SedonaScalarKernel for RsMetaData {
 
         with_gdal(|gdal| {
             configure_thread_local_options(gdal, config_options)?;
-            let provider = thread_local_provider(gdal)
-                .map_err(|e| exec_datafusion_err!("Failed to init GDAL provider: {e}"))?;
+            let provider = thread_local_provider(gdal).context("Failed to init GDAL provider")?;
 
             executor.execute_raster_void(|_i, raster_opt| {
                 match raster_opt {
@@ -162,13 +161,13 @@ impl SedonaScalarKernel for RsMetaData {
                             tile_width_builder.append_value(0);
                             tile_height_builder.append_value(0);
                         } else {
-                            let dataset = provider.raster_ref_to_gdal(raster).map_err(|e| {
-                                exec_datafusion_err!("Failed to create GDAL dataset: {e}")
-                            })?;
+                            let dataset = provider
+                                .raster_ref_to_gdal(raster)
+                                .context("Failed to create GDAL dataset")?;
                             let band1 = dataset
                                 .as_dataset()
                                 .rasterband(1)
-                                .map_err(|e| exec_datafusion_err!("Failed to get band 1: {e}"))?;
+                                .context("Failed to get band 1")?;
                             let (block_x, block_y) = band1.block_size();
                             tile_width_builder.append_value(block_x as u64);
                             tile_height_builder.append_value(block_y as u64);
diff --git a/rust/sedona-raster-gdal/src/rs_polygonize.rs b/rust/sedona-raster-gdal/src/rs_polygonize.rs
index 49a087117..b15aa94ae 100644
--- a/rust/sedona-raster-gdal/src/rs_polygonize.rs
+++ b/rust/sedona-raster-gdal/src/rs_polygonize.rs
@@ -35,6 +35,7 @@ use sedona_gdal::gdal::Gdal;
 use sedona_gdal::gdal_dyn_bindgen::{OGRFieldType, OGRwkbGeometryType};
 use sedona_gdal::raster::polygonize::PolygonizeOptions;
 use sedona_gdal::raster::types::GdalDataType;
+use sedona_raster::error::RasterResultExt;
 use sedona_raster::traits::RasterRef;
 use sedona_raster_functions::RasterExecutor;
 use sedona_schema::datatypes::{SedonaType, WKB_GEOMETRY_ITEM_CRS};
@@ -94,12 +95,11 @@ impl SedonaScalarKernel for RsPolygonize {
 
         with_gdal(|gdal| {
             configure_thread_local_options(gdal, config_options)?;
-            let provider = thread_local_provider(gdal)
-                .map_err(|e| exec_datafusion_err!("Failed to init GDAL provider: {e}"))?;
+            let provider = thread_local_provider(gdal).context("Failed to init GDAL provider")?;
             // Note: We deliberately use "Memory" instead of "MEM" to be compatible with older GDAL versions.
             let mem_driver = gdal
                 .get_driver_by_name("Memory")
-                .map_err(|e| exec_datafusion_err!("Failed to get Memory driver: {e}"))?;
+                .context("Failed to get Memory driver")?;
 
             executor.execute_raster_void(|_, raster_opt| {
                 let band_opt = band_iter.next().expect("band iteration should match rows");
@@ -122,7 +122,7 @@ impl SedonaScalarKernel for RsPolygonize {
 
                 let raster_ds = provider
                     .raster_ref_to_gdal(raster)
-                    .map_err(|e| exec_datafusion_err!("Failed to create GDAL dataset: {e}"))?;
+                    .context("Failed to create GDAL dataset")?;
 
                 let crs_str = raster.crs();
                 let mut num_polygons = 0;
@@ -234,12 +234,12 @@ where
 
     let raster_band = gdal_dataset
         .rasterband(band_num)
-        .map_err(|e| exec_datafusion_err!("Failed to get band {}: {}", band_num, e))?;
+        .with_context(|| format!("Failed to get band {band_num}"))?;
 
     // Create a memory dataset containing one vector layer to hold the polygonized output.
     let vector_ds = mem_driver
         .create_vector_only("")
-        .map_err(|e| exec_datafusion_err!("Failed to create vector dataset: {e}"))?;
+        .context("Failed to create vector dataset")?;
 
     let spatial_ref = gdal_dataset.spatial_ref().ok();
     let mut layer = vector_ds
@@ -249,14 +249,14 @@ where
             ty: OGRwkbGeometryType::wkbPolygon,
             options: None,
         })
-        .map_err(|e| exec_datafusion_err!("Failed to create layer: {e}"))?;
+        .context("Failed to create layer")?;
 
     let field_defn = gdal
         .create_field_defn("value", OGRFieldType::OFTReal)
-        .map_err(|e| exec_datafusion_err!("Failed to create field definition: {e}"))?;
+        .context("Failed to create field definition")?;
     layer
         .create_field(&field_defn)
-        .map_err(|e| exec_datafusion_err!("Failed to add field to layer: {e}"))?;
+        .context("Failed to add field to layer")?;
 
     // Polygonize the raster band into the vector layer, using the "value" field to store pixel values.
     let band_type = raster_band.band_type();
@@ -264,10 +264,10 @@ where
 
     if is_float {
         gdal.fpolygonize(&raster_band, None, &layer, 0, &PolygonizeOptions::default())
-            .map_err(|e| exec_datafusion_err!("GDAL fpolygonize failed: {e}"))?;
+            .context("GDAL fpolygonize failed")?;
     } else {
         gdal.polygonize(&raster_band, None, &layer, 0, &PolygonizeOptions::default())
-            .map_err(|e| exec_datafusion_err!("GDAL polygonize failed: {e}"))?;
+            .context("GDAL polygonize failed")?;
     }
 
     // Extract the WKB geometry and value for each feature in the layer.
@@ -276,16 +276,14 @@ where
         let geom = feature
             .geometry()
             .ok_or_else(|| exec_datafusion_err!("Polygonize output feature missing geometry"))?;
-        let wkb = geom
-            .wkb()
-            .map_err(|e| exec_datafusion_err!("Failed to export geometry to WKB: {e}"))?;
+        let wkb = geom.wkb().context("Failed to export geometry to WKB")?;
 
         let idx = match value_field_idx {
             Some(idx) => idx,
             None => {
                 let idx = feature
                     .field_index("value")
-                    .map_err(|e| exec_datafusion_err!("Missing 'value' field: {e}"))?;
+                    .context("Missing 'value' field")?;
                 value_field_idx = Some(idx);
                 idx
             }
diff --git a/rust/sedona-raster-gdal/src/rs_tile.rs b/rust/sedona-raster-gdal/src/rs_tile.rs
index d3044be1c..c96b950d8 100644
--- a/rust/sedona-raster-gdal/src/rs_tile.rs
+++ b/rust/sedona-raster-gdal/src/rs_tile.rs
@@ -43,6 +43,7 @@ use sedona_common::sedona_internal_err;
 use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
 use sedona_raster::array::RasterRefImpl;
 use sedona_raster::builder::RasterBuilder;
+use sedona_raster::error::RasterResultExt;
 use sedona_raster::geo_transform::{GeoTransform, GeoTransformEx};
 use sedona_raster::traits::{is_spatial_dim_pair, nodata_f64_to_bytes, RasterRef};
 use sedona_raster_functions::rs_ensure_loaded::NEEDS_PIXELS_METADATA_KEY;
@@ -355,9 +356,7 @@ impl RsTile {
 /// The `List<Struct<x, y, tile>>` element struct fields, mirroring Spark's
 /// `(x int, y int, tile raster)` generator output.
 fn tile_struct_fields() -> Result<Fields> {
-    let tile_field = RASTER
-        .to_storage_field("tile", false)
-        .map_err(|e| exec_datafusion_err!("RS_Tile: {e}"))?;
+    let tile_field = RASTER.to_storage_field("tile", false).context("RS_Tile")?;
     Ok(Fields::from(vec![
         Field::new("x", DataType::Int32, false),
         Field::new("y", DataType::Int32, false),
@@ -558,7 +557,7 @@ fn append_tile(
             &tile_spatial_shape,
             raster.crs(),
         )
-        .map_err(|e| exec_datafusion_err!("RS_Tile: failed to start raster: {e}"))?;
+        .context("RS_Tile: failed to start raster")?;
 
     for &band_idx in band_indices {
         append_tile_band(raster, band_idx, window, params, rast_builder)?;
@@ -566,7 +565,7 @@ fn append_tile(
 
     rast_builder
         .finish_raster()
-        .map_err(|e| exec_datafusion_err!("RS_Tile: failed to finish raster: {e}"))?;
+        .context("RS_Tile: failed to finish raster")?;
     Ok(())
 }
 
@@ -581,7 +580,7 @@ fn append_tile_band(
     // `band_idx` is 1-based; the `band`/`band_name` accessors are 0-based.
     let band = raster
         .band(band_idx - 1)
-        .map_err(|e| exec_datafusion_err!("RS_Tile: failed to get band {band_idx}: {e}"))?;
+        .with_context(|| format!("RS_Tile: failed to get band {band_idx}"))?;
     let band_name = raster.band_name(band_idx - 1).map(|s| s.to_string());
 
     let data_type = band.data_type();
@@ -617,10 +616,10 @@ fn append_tile_band(
     // the tile's own buffer, so no copy of the source is needed here).
     let nd_buffer = band
         .nd_buffer()
-        .map_err(|e| exec_datafusion_err!("RS_Tile: failed to read band {band_idx}: {e}"))?;
+        .with_context(|| format!("RS_Tile: failed to read band {band_idx}"))?;
     let source = nd_buffer
         .as_contiguous()
-        .map_err(|e| exec_datafusion_err!("RS_Tile: band {band_idx} is not contiguous: {e}"))?;
+        .with_context(|| format!("RS_Tile: band {band_idx} is not contiguous"))?;
     let in_plane_bytes = width * height * byte_size;
     if source.len() != n_planes * in_plane_bytes {
         return exec_err!(
@@ -637,9 +636,8 @@ fn append_tile_band(
     // doesn't fit the band dtype errors rather than silently saturating.
     let (pad_fill, tile_nodata): (Option<Vec<u8>>, Option<Vec<u8>>) = if window.needs_padding() {
         let fill = match params.nodata {
-            Some(value) => nodata_f64_to_bytes(value, &data_type).map_err(|e| {
-                exec_datafusion_err!("RS_Tile: invalid nodata for band {band_idx}: {e}")
-            })?,
+            Some(value) => nodata_f64_to_bytes(value, &data_type)
+                .with_context(|| format!("RS_Tile: invalid nodata for band {band_idx}"))?,
             None => match band.nodata() {
                 Some(bytes) => bytes.to_vec(),
                 None => data_type.min_value_le_bytes(),
@@ -755,13 +753,13 @@ fn assemble_tile_list(
     let tile_array: ArrayRef = Arc::new(
         rast_builder
             .finish()
-            .map_err(|e| exec_datafusion_err!("RS_Tile: failed to build tiles: {e}"))?,
+            .context("RS_Tile: failed to build tiles")?,
     );
 
     let fields = tile_struct_fields()?;
     let element_struct =
         StructArray::try_new(fields.clone(), vec![x_array, y_array, tile_array], None)
-            .map_err(|e| exec_datafusion_err!("RS_Tile: failed to build tile struct: {e}"))?;
+            .context("RS_Tile: failed to build tile struct")?;
 
     let list_field = Arc::new(Field::new("item", DataType::Struct(fields), true));
     ListArray::try_new(
diff --git a/rust/sedona-raster-gdal/src/rs_zonal_stats.rs b/rust/sedona-raster-gdal/src/rs_zonal_stats.rs
index 138e28ca0..e32190c98 100644
--- a/rust/sedona-raster-gdal/src/rs_zonal_stats.rs
+++ b/rust/sedona-raster-gdal/src/rs_zonal_stats.rs
@@ -64,6 +64,7 @@ use sedona_common::sedona_internal_err;
 use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
 use sedona_gdal::gdal::Gdal;
 use sedona_raster::array::RasterRefImpl;
+use sedona_raster::error::RasterResultExt;
 use sedona_raster::traits::RasterRef;
 use sedona_raster_functions::crs_utils::{align_wkb_to_crs, resolve_crs, with_crs_engine};
 use sedona_raster_functions::rs_ensure_loaded::NEEDS_PIXELS_METADATA_KEY;
@@ -671,7 +672,7 @@ fn collect_zonal_values(
     // `band_num` is 1-based (>= 1, guaranteed by `resolve_band`); `band` is 0-based.
     let band = raster
         .band(band_num - 1)
-        .map_err(|e| exec_datafusion_err!("RS_ZonalStats: failed to read band {band_num}: {e}"))?;
+        .with_context(|| format!("RS_ZonalStats: failed to read band {band_num}"))?;
     if !band.is_spatial_2d() {
         return exec_err!(
             "RS_ZonalStats supports 2-D rasters only; band {band_num} is not a 2-D (y, x) grid"
@@ -701,7 +702,7 @@ fn collect_zonal_values(
     // pixels, so it is count 0 rather than no-intersection.
     let geometry = gdal
         .geometry_from_wkb(geom_wkb)
-        .map_err(|e| exec_datafusion_err!("RS_ZonalStats: failed to parse geometry: {e}"))?;
+        .context("RS_ZonalStats: failed to parse geometry")?;
     let Some(window) = envelope_window(&geometry, &transform, width, height)? else {
         scratch.clear();
         return Ok(RoiCoverage::Collected);
@@ -722,10 +723,10 @@ fn collect_zonal_values(
     // Read the band once (zero-copy borrow) and collect the selected values.
     let nd_buffer = band
         .nd_buffer()
-        .map_err(|e| exec_datafusion_err!("RS_ZonalStats: failed to read band {band_num}: {e}"))?;
-    let band_bytes = nd_buffer.as_contiguous().map_err(|e| {
-        exec_datafusion_err!("RS_ZonalStats: band {band_num} is not contiguous: {e}")
-    })?;
+        .with_context(|| format!("RS_ZonalStats: failed to read band {band_num}"))?;
+    let band_bytes = nd_buffer
+        .as_contiguous()
+        .with_context(|| format!("RS_ZonalStats: band {band_num} is not contiguous"))?;
     let expected = width
         .checked_mul(height)
         .and_then(|n| n.checked_mul(byte_size))
diff --git a/rust/sedona-raster-gdal/src/utils.rs b/rust/sedona-raster-gdal/src/utils.rs
index 27d5a067f..972dbc875 100644
--- a/rust/sedona-raster-gdal/src/utils.rs
+++ b/rust/sedona-raster-gdal/src/utils.rs
@@ -34,6 +34,7 @@ use arrow_schema::ArrowError;
 use sedona_common::sedona_internal_err;
 use sedona_raster::array::RasterRefImpl;
 use sedona_raster::builder::{RasterBuilder, StartBandArgs};
+use sedona_raster::error::RasterResultExt;
 use sedona_raster::traits::RasterRef;
 use sedona_schema::raster::BandDataType;
 
@@ -48,7 +49,7 @@ pub fn append_as_indb_raster(dataset: &Dataset, builder: &mut RasterBuilder) ->
 
     let geotransform = dataset
         .geo_transform()
-        .map_err(|e| exec_datafusion_err!("Failed to get geotransform: {}", e))?;
+        .context("Failed to get geotransform")?;
 
     let grid = Grid::from_gdal(geotransform, width, height);
 
@@ -58,13 +59,13 @@ pub fn append_as_indb_raster(dataset: &Dataset, builder: &mut RasterBuilder) ->
         .and_then(|sr: SpatialRef| sr.to_projjson().ok());
 
     grid.start_raster_into(builder, crs.as_deref())
-        .map_err(|e| exec_datafusion_err!("Failed to start raster: {}", e))?;
+        .context("Failed to start raster")?;
 
     let band_count = dataset.raster_count();
     for band_idx in 1..=band_count {
         let band = dataset
             .rasterband(band_idx)
-            .map_err(|e| exec_datafusion_err!("Failed to get band {}: {}", band_idx, e))?;
+            .with_context(|| format!("Failed to get band {band_idx}"))?;
 
         let gdal_type = band.band_type();
         let band_data_type = gdal_to_band_data_type(gdal_type)
@@ -74,7 +75,7 @@ pub fn append_as_indb_raster(dataset: &Dataset, builder: &mut RasterBuilder) ->
 
         let band_data = band
             .read_as_bytes((0, 0), (width, height), (width, height), None)
-            .map_err(|e| exec_datafusion_err!("Failed to read band {} data: {}", band_idx, e))?;
+            .with_context(|| format!("Failed to read band {band_idx} data"))?;
         // Single-band native read: the 2-D `["y", "x"]` band `start_band_2d`
         // would build, shape `[height, width]`.
         append_band_from_buffer(
@@ -90,9 +91,7 @@ pub fn append_as_indb_raster(dataset: &Dataset, builder: &mut RasterBuilder) ->
         )?;
     }
 
-    builder
-        .finish_raster()
-        .map_err(|e| exec_datafusion_err!("Failed to finish raster: {}", e))?;
+    builder.finish_raster().context("Failed to finish raster")?;
 
     Ok(())
 }
@@ -108,19 +107,14 @@ pub fn append_as_outdb_raster(gdal: &Gdal, path: &str, builder: &mut RasterBuild
                 ..Default::default()
             },
         )
-        .map_err(|e| {
-            exec_datafusion_err!(
-                "Failed to open raster file '{}' (GDAL path '{}'): {}",
-                path,
-                gdal_path,
-                e
-            )
+        .with_context(|| {
+            format!("Failed to open raster file '{path}' (GDAL path '{gdal_path}')")
         })?;
 
     let (width, height) = dataset.raster_size();
     let geotransform = dataset
         .geo_transform()
-        .map_err(|e| exec_datafusion_err!("Failed to get geotransform: {}", e))?;
+        .context("Failed to get geotransform")?;
     let grid = Grid::from_gdal(geotransform, width, height);
 
     let crs = dataset
@@ -134,7 +128,7 @@ pub fn append_as_outdb_raster(gdal: &Gdal, path: &str, builder: &mut RasterBuild
     for band_idx in 1..=band_count {
         let band = dataset
             .rasterband(band_idx)
-            .map_err(|e| exec_datafusion_err!("Failed to get band {}: {}", band_idx, e))?;
+            .with_context(|| format!("Failed to get band {band_idx}"))?;
 
         let gdal_type = band.band_type();
         let band_data_type = gdal_to_band_data_type(gdal_type)
@@ -185,7 +179,7 @@ pub fn append_nd_from_dataset(
 
     let geotransform = dataset
         .geo_transform()
-        .map_err(|e| exec_datafusion_err!("Failed to get geotransform: {}", e))?;
+        .context("Failed to get geotransform")?;
     let grid = Grid::from_gdal(geotransform, width, height);
 
     let crs = dataset
@@ -249,17 +243,19 @@ impl Grid {
         crs: Option<&str>,
     ) -> Result<(), ArrowError> {
         let t = self.transform;
-        builder.start_raster_2d(
-            self.width,
-            self.height,
-            t[0],
-            t[3],
-            t[1],
-            t[5],
-            t[2],
-            t[4],
-            crs,
-        )
+        builder
+            .start_raster_2d(
+                self.width,
+                self.height,
+                t[0],
+                t[3],
+                t[1],
+                t[5],
+                t[2],
+                t[4],
+                crs,
+            )
+            .map_err(Into::into)
     }
 
     /// Bounding box of the four grid corners (handles skew; reduces to
@@ -564,16 +560,14 @@ pub(crate) fn append_band_from_buffer(
             nodata: header.nodata,
             ..StartBandArgs::new(header.dim_names, header.shape, header.data_type)
         })
-        .map_err(|e| exec_datafusion_err!("Failed to start band: {e}"))?;
+        .context("Failed to start band")?;
     // Hand the owned buffer to Arrow as a shared data block (a refcount bump,
     // never a copy). `append_band_data_buffer` also stores sub-inline-threshold
     // bands inline, keeping the view canonical.
     builder
         .append_band_data_buffer(&Buffer::from(band_data), 0, band_data_len)
-        .map_err(|e| exec_datafusion_err!("Failed to append band data: {e}"))?;
-    builder
-        .finish_band()
-        .map_err(|e| exec_datafusion_err!("Failed to finish band: {e}"))?;
+        .context("Failed to append band data")?;
+    builder.finish_band().context("Failed to finish band")?;
     Ok(())
 }
 
@@ -671,7 +665,7 @@ fn append_nd_from_dataset_inner(
     let out_height = grid.height as usize;
 
     grid.start_raster_into(builder, crs)
-        .map_err(|e| exec_datafusion_err!("Failed to start raster: {}", e))?;
+        .context("Failed to start raster")?;
 
     let total_planes: usize = layout.bands.iter().map(|b| b.plane_count).sum();
     let gdal_band_count = dataset.raster_count();
@@ -706,7 +700,7 @@ fn append_nd_from_dataset_inner(
                 let gdal_band = gdal_band_base + plane;
                 let band = dataset
                     .rasterband(gdal_band)
-                    .map_err(|e| exec_datafusion_err!("Failed to get band {gdal_band}: {e}"))?;
+                    .with_context(|| format!("Failed to get band {gdal_band}"))?;
                 let plane = band
                     .read_as_bytes(
                         (0, 0),
@@ -714,9 +708,7 @@ fn append_nd_from_dataset_inner(
                         (out_width, out_height),
                         alg,
                     )
-                    .map_err(|e| {
-                        exec_datafusion_err!("Failed to read band {gdal_band} data: {e}")
-                    })?;
+                    .with_context(|| format!("Failed to read band {gdal_band} data"))?;
                 out.extend_from_slice(&plane);
                 Ok(())
             },
@@ -724,9 +716,7 @@ fn append_nd_from_dataset_inner(
         gdal_band += plan.plane_count;
     }
 
-    builder
-        .finish_raster()
-        .map_err(|e| exec_datafusion_err!("Failed to finish raster: {}", e))?;
+    builder.finish_raster().context("Failed to finish raster")?;
 
     Ok(())
 }
diff --git a/rust/sedona-raster-zarr/src/loader.rs b/rust/sedona-raster-zarr/src/loader.rs
index 9a1db9667..4cbacf1cb 100644
--- a/rust/sedona-raster-zarr/src/loader.rs
+++ b/rust/sedona-raster-zarr/src/loader.rs
@@ -186,7 +186,7 @@ impl ZarrChunkReader {
             builder.band_data_writer().append_value([0u8; 0]);
             builder.finish_band()?;
         }
-        builder.finish_raster()
+        Ok(builder.finish_raster()?)
     }
 }
 
@@ -218,7 +218,7 @@ impl Iterator for ZarrChunkReader {
         }
         let struct_arr = match builder.finish() {
             Ok(arr) => arr,
-            Err(e) => return Some(Err(e)),
+            Err(e) => return Some(Err(e.into())),
         };
         Some(RecordBatch::try_new(
             self.schema.clone(),
diff --git a/rust/sedona-raster/Cargo.toml b/rust/sedona-raster/Cargo.toml
index f6b692429..d728b33bb 100644
--- a/rust/sedona-raster/Cargo.toml
+++ b/rust/sedona-raster/Cargo.toml
@@ -38,6 +38,7 @@ async-trait = { workspace = true }
 datafusion-common = { workspace = true }
 sedona-common = { workspace = true }
 sedona-schema = { workspace = true }
+thiserror = { workspace = true }
 
 [dev-dependencies]
 sedona-testing = { workspace = true }
diff --git a/rust/sedona-raster/src/affine_transformation.rs b/rust/sedona-raster/src/affine_transformation.rs
index 7575abf78..8577d5a69 100644
--- a/rust/sedona-raster/src/affine_transformation.rs
+++ b/rust/sedona-raster/src/affine_transformation.rs
@@ -24,9 +24,9 @@
 //!
 //! [`GeoTransform`]: crate::geo_transform::GeoTransform
 
+use crate::error::RasterError;
 use crate::geo_transform::GeoTransformEx;
 use crate::traits::RasterRef;
-use arrow_schema::ArrowError;
 
 /// Computes the rotation angle (in radians) of the raster based on its geotransform metadata.
 #[inline]
@@ -57,11 +57,9 @@ pub fn to_raster_coordinate(
     raster: &dyn RasterRef,
     world_x: f64,
     world_y: f64,
-) -> Result<(i64, i64), ArrowError> {
+) -> Result<(i64, i64), RasterError> {
     let inverse = raster.transform().invert().map_err(|_| {
-        ArrowError::InvalidArgumentError(
-            "Cannot compute coordinate: determinant is zero.".to_string(),
-        )
+        RasterError::Invalid("Cannot compute coordinate: determinant is zero.".to_string())
     })?;
     let (rx, ry) = inverse.apply(world_x, world_y);
     Ok((rx as i64, ry as i64))
@@ -87,8 +85,8 @@ mod tests {
         fn num_bands(&self) -> usize {
             0
         }
-        fn band(&self, index: usize) -> Result<Box<dyn BandRef + '_>, ArrowError> {
-            Err(ArrowError::InvalidArgumentError(format!(
+        fn band(&self, index: usize) -> Result<Box<dyn BandRef + '_>, RasterError> {
+            Err(RasterError::Invalid(format!(
                 "Band index {index} is out of range: this raster has 0 bands"
             )))
         }
diff --git a/rust/sedona-raster/src/array.rs b/rust/sedona-raster/src/array.rs
index 482df92cd..a4ea487c4 100644
--- a/rust/sedona-raster/src/array.rs
+++ b/rust/sedona-raster/src/array.rs
@@ -15,11 +15,11 @@
 // specific language governing permissions and limitations
 // under the License.
 
+use crate::error::RasterError;
 use arrow_array::{
     Array, BinaryArray, BinaryViewArray, Float64Array, Int64Array, ListArray, StringArray,
     StringViewArray, StructArray, UInt32Array,
 };
-use arrow_schema::ArrowError;
 use datafusion_common::cast::{
     as_binary_array, as_binary_view_array, as_float64_array, as_int64_array, as_list_array,
     as_string_array, as_string_view_array, as_struct_array, as_uint32_array,
@@ -121,9 +121,9 @@ impl<'a> BandRef for BandRefImpl<'a> {
             || !self.data_array.value(self.band_row).is_empty()
     }
 
-    fn nd_buffer(&self) -> Result<NdBuffer<'_>, ArrowError> {
+    fn nd_buffer(&self) -> Result<NdBuffer<'_>, RasterError> {
         if !self.is_indb() {
-            return Err(ArrowError::NotYetImplemented(
+            return Err(RasterError::Invalid(
                 "OutDb byte access via nd_buffer() is not yet implemented; \
                  backend-specific OutDb resolvers are tracked separately"
                     .to_string(),
@@ -143,7 +143,7 @@ impl<'a> BandRef for BandRefImpl<'a> {
     /// Zero-copy override: share the source row's backing `Buffer` into the
     /// builder (refcount bump) instead of copying the visible bytes. OutDb
     /// bands have an empty data column by design.
-    fn append_data_into(&self, builder: &mut RasterBuilder) -> Result<(), ArrowError> {
+    fn append_data_into(&self, builder: &mut RasterBuilder) -> Result<(), RasterError> {
         if self.is_indb() {
             builder.append_band_data_from(self.data_array, self.band_row)
         } else {
@@ -199,10 +199,10 @@ impl<'a> RasterRef for RasterRefImpl<'a> {
         self.bands_list.value_length(self.raster_index) as usize
     }
 
-    fn band(&self, index: usize) -> Result<Box<dyn BandRef + '_>, ArrowError> {
+    fn band(&self, index: usize) -> Result<Box<dyn BandRef + '_>, RasterError> {
         let nbands = self.num_bands();
         if index >= nbands {
-            return Err(ArrowError::InvalidArgumentError(format!(
+            return Err(RasterError::Invalid(format!(
                 "Band index {index} is out of range: this raster has {nbands} bands"
             )));
         }
@@ -217,7 +217,7 @@ impl<'a> RasterRef for RasterRefImpl<'a> {
         // Reject 0-D bands at the read boundary. Schema doesn't forbid them
         // outright but every consumer assumes ndim >= 1.
         if source_shape.is_empty() {
-            return Err(ArrowError::ExternalError(Box::new(
+            return Err(RasterError::External(Box::new(
                 sedona_common::sedona_internal_datafusion_err!(
                     "band {band_row} has empty source_shape; ndim must be >= 1"
                 ),
@@ -229,7 +229,7 @@ impl<'a> RasterRef for RasterRefImpl<'a> {
         // here is appropriate.
         let data_type_value = self.band_datatype_array.value(band_row);
         let data_type = BandDataType::try_from_u32(data_type_value).ok_or_else(|| {
-            ArrowError::ExternalError(Box::new(sedona_common::sedona_internal_datafusion_err!(
+            RasterError::External(Box::new(sedona_common::sedona_internal_datafusion_err!(
                 "band {band_row} has unknown data_type discriminant {data_type_value}"
             )))
         })?;
@@ -246,7 +246,7 @@ impl<'a> RasterRef for RasterRefImpl<'a> {
         // request/response must round-trip the view — tracked in
         // <https://github.com/apache/sedona-db/issues/897>.
         if !self.band_view_list.is_null(band_row) {
-            return Err(ArrowError::ExternalError(Box::new(
+            return Err(RasterError::External(Box::new(
                 sedona_common::sedona_internal_datafusion_err!(
                     "non-null view row at band {band_row}: view composition is not yet implemented"
                 ),
@@ -418,9 +418,9 @@ impl<'a> RasterStructArray<'a> {
     ///
     /// Returns an error if the array doesn't have the expected raster schema.
     #[inline]
-    pub fn try_new(raster_array: &'a StructArray) -> Result<Self, ArrowError> {
+    pub fn try_new(raster_array: &'a StructArray) -> Result<Self, RasterError> {
         if raster_array.fields().len() != raster_indices::FIELD_COUNT {
-            return Err(ArrowError::SchemaError(
+            return Err(RasterError::Invalid(
                 "Unexpected column count for raster array".to_string(),
             ));
         }
@@ -439,7 +439,7 @@ impl<'a> RasterStructArray<'a> {
         let bands_struct = as_struct_array(bands_list.values())?;
 
         if bands_struct.fields().len() != band_indices::FIELD_COUNT {
-            return Err(ArrowError::SchemaError(
+            return Err(RasterError::Invalid(
                 "Unexpected column count for band array".to_string(),
             ));
         }
@@ -497,9 +497,9 @@ impl<'a> RasterStructArray<'a> {
 
     /// Get a specific raster by index.
     #[inline(always)]
-    pub fn get(&self, index: usize) -> Result<RasterRefImpl<'a>, ArrowError> {
+    pub fn get(&self, index: usize) -> Result<RasterRefImpl<'a>, RasterError> {
         if index >= self.raster_array.len() {
-            return Err(ArrowError::InvalidArgumentError(format!(
+            return Err(RasterError::Invalid(format!(
                 "Invalid raster index: {index}"
             )));
         }
@@ -869,7 +869,7 @@ mod tests {
         let rasters = RasterStructArray::try_new(&mutated).unwrap();
         let r = rasters.get(0).unwrap();
         // band() surfaces the corruption through the standardized
-        // SedonaDB-internal-error message routed via ArrowError::ExternalError.
+        // SedonaDB-internal-error message routed via RasterError::External.
         // `Box<dyn BandRef>` isn't `Debug`, so unwrap_err doesn't compile —
         // pull the error out via `.err().unwrap()` on the `Option<E>` side.
         let err = r.band(0).err().unwrap();
diff --git a/rust/sedona-raster/src/builder.rs b/rust/sedona-raster/src/builder.rs
index 76782f9c8..65accd97c 100644
--- a/rust/sedona-raster/src/builder.rs
+++ b/rust/sedona-raster/src/builder.rs
@@ -15,6 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
+use crate::error::RasterError;
 use arrow_array::{
     builder::{
         ArrayBuilder, BinaryBuilder, BinaryViewBuilder, BooleanBuilder, Float64Builder,
@@ -23,7 +24,7 @@ use arrow_array::{
     Array, ArrayRef, BinaryViewArray, ListArray, StructArray,
 };
 use arrow_buffer::{Buffer, NullBuffer, OffsetBuffer, ScalarBuffer};
-use arrow_schema::{ArrowError, DataType};
+use arrow_schema::DataType;
 use std::collections::HashMap;
 use std::sync::Arc;
 
@@ -237,9 +238,9 @@ impl RasterBuilder {
         spatial_dims: &[&str],
         spatial_shape: &[i64],
         crs: Option<&str>,
-    ) -> Result<(), ArrowError> {
+    ) -> Result<(), RasterError> {
         if spatial_dims.len() != spatial_shape.len() {
-            return Err(ArrowError::InvalidArgumentError(format!(
+            return Err(RasterError::Invalid(format!(
                 "spatial_dims.len() ({}) must equal spatial_shape.len() ({})",
                 spatial_dims.len(),
                 spatial_shape.len()
@@ -296,11 +297,11 @@ impl RasterBuilder {
         &mut self,
         source: &dyn RasterRef,
         overrides: RasterOverrides,
-    ) -> Result<(), ArrowError> {
+    ) -> Result<(), RasterError> {
         let transform: [f64; 6] = match overrides.transform {
             Some(transform) => transform,
             None => source.transform().try_into().map_err(|_| {
-                ArrowError::InvalidArgumentError("raster transform is not 6 elements".to_string())
+                RasterError::Invalid("raster transform is not 6 elements".to_string())
             })?,
         };
         let spatial_dims = source.spatial_dims();
@@ -321,7 +322,7 @@ impl RasterBuilder {
         &mut self,
         source: &dyn RasterRef,
         overrides: RasterOverrides,
-    ) -> Result<(), ArrowError> {
+    ) -> Result<(), RasterError> {
         self.start_raster_from(source, overrides)?;
         for band_idx in 0..source.num_bands() {
             source
@@ -349,7 +350,7 @@ impl RasterBuilder {
         skew_x: f64,
         skew_y: f64,
         crs: Option<&str>,
-    ) -> Result<(), ArrowError> {
+    ) -> Result<(), RasterError> {
         let transform = [origin_x, scale_x, skew_x, origin_y, skew_y, scale_y];
         self.start_raster_nd(&transform, &["x", "y"], &[width, height], crs)?;
         self.current_width = width;
@@ -372,7 +373,7 @@ impl RasterBuilder {
     /// that identity null sentinel, so an identity `Some` view is stored the
     /// same as `None` and a non-identity view is rejected. View persistence is
     /// tracked in <https://github.com/apache/sedona-db/issues/897>.
-    pub fn start_band(&mut self, args: StartBandArgs<'_>) -> Result<(), ArrowError> {
+    pub fn start_band(&mut self, args: StartBandArgs<'_>) -> Result<(), RasterError> {
         let StartBandArgs {
             name,
             dim_names,
@@ -391,12 +392,12 @@ impl RasterBuilder {
         if let Some(view) = view {
             let ndim = dim_names.len();
             if ndim == 0 {
-                return Err(ArrowError::InvalidArgumentError(
+                return Err(RasterError::Invalid(
                     "start_band: 0-dimensional bands are not supported".into(),
                 ));
             }
             if source_shape.len() != ndim || view.len() != ndim {
-                return Err(ArrowError::InvalidArgumentError(format!(
+                return Err(RasterError::Invalid(format!(
                     "start_band: dim_names ({}), source_shape ({}), and view ({}) \
                      must all have the same length",
                     ndim,
@@ -407,7 +408,7 @@ impl RasterBuilder {
             let view_entries = ViewEntries::new(view.to_vec());
             view_entries.validate(source_shape)?;
             if !view_entries.is_identity(source_shape) {
-                return Err(ArrowError::InvalidArgumentError(
+                return Err(RasterError::Invalid(
                     "start_band: persisting a non-identity band view is not yet \
                      supported (see https://github.com/apache/sedona-db/issues/897); \
                      materialize the band (e.g. via RS_EnsureContiguous) first"
@@ -417,12 +418,12 @@ impl RasterBuilder {
         }
 
         if dim_names.is_empty() {
-            return Err(ArrowError::InvalidArgumentError(
+            return Err(RasterError::Invalid(
                 "start_band: 0-dimensional bands are not supported".into(),
             ));
         }
         if dim_names.len() != source_shape.len() {
-            return Err(ArrowError::InvalidArgumentError(format!(
+            return Err(RasterError::Invalid(format!(
                 "start_band: dim_names ({}) and shape ({}) must have the same length",
                 dim_names.len(),
                 source_shape.len(),
@@ -495,9 +496,9 @@ impl RasterBuilder {
         &mut self,
         data_type: BandDataType,
         nodata: Option<&[u8]>,
-    ) -> Result<(), ArrowError> {
+    ) -> Result<(), RasterError> {
         if self.current_width == 0 && self.current_height == 0 {
-            return Err(ArrowError::InvalidArgumentError(
+            return Err(RasterError::Invalid(
                 "start_band_2d requires prior start_raster_2d (width and height are 0)".into(),
             ));
         }
@@ -539,7 +540,7 @@ impl RasterBuilder {
         buffer: &Buffer,
         offset: u32,
         len: u32,
-    ) -> Result<(), ArrowError> {
+    ) -> Result<(), RasterError> {
         if len <= MAX_INLINE_VIEW_LEN {
             self.band_data
                 .append_value(&buffer.as_slice()[offset as usize..(offset + len) as usize]);
@@ -554,7 +555,7 @@ impl RasterBuilder {
                 idx
             }
         };
-        self.band_data.try_append_view(block, offset, len)
+        Ok(self.band_data.try_append_view(block, offset, len)?)
     }
 
     /// Append the current band's data by copying row `row` of `src` through —
@@ -570,7 +571,7 @@ impl RasterBuilder {
         &mut self,
         src: &BinaryViewArray,
         row: usize,
-    ) -> Result<(), ArrowError> {
+    ) -> Result<(), RasterError> {
         // Arrow BYTE_VIEW layout (u128, little-endian fields), fixed by the
         // columnar format spec:
         //   bits   0..32  length
@@ -594,10 +595,10 @@ impl RasterBuilder {
     /// Finish writing the current band.
     ///
     /// Validates that exactly one data value was appended since `start_band()`.
-    pub fn finish_band(&mut self) -> Result<(), ArrowError> {
+    pub fn finish_band(&mut self) -> Result<(), RasterError> {
         let current_count = self.band_data.len();
         if current_count != self.band_data_count_at_start + 1 {
-            return Err(ArrowError::InvalidArgumentError(
+            return Err(RasterError::Invalid(
                 format!(
                     "Expected exactly one band data value per band, but got {} appended since start_band()",
                     current_count - self.band_data_count_at_start
@@ -612,14 +613,14 @@ impl RasterBuilder {
     /// Strictly validates every band added since `start_raster_nd`: each name in
     /// the top-level `spatial_dims` must appear in the band's own `dim_names`
     /// with a size matching the corresponding entry in `spatial_shape`.
-    pub fn finish_raster(&mut self) -> Result<(), ArrowError> {
+    pub fn finish_raster(&mut self) -> Result<(), RasterError> {
         for (band_idx, (band_dims, band_shape)) in self.current_raster_bands.iter().enumerate() {
             for (spatial_idx, spatial_dim) in self.current_spatial_dims.iter().enumerate() {
                 let pos = band_dims
                     .iter()
                     .position(|d| d == spatial_dim)
                     .ok_or_else(|| {
-                        ArrowError::InvalidArgumentError(format!(
+                        RasterError::Invalid(format!(
                             "Band {band_idx} is missing spatial dimension {spatial_dim:?} \
                          (band dim_names = {band_dims:?})"
                         ))
@@ -627,7 +628,7 @@ impl RasterBuilder {
                 let expected = self.current_spatial_shape[spatial_idx];
                 let actual = band_shape[pos];
                 if actual != expected {
-                    return Err(ArrowError::InvalidArgumentError(format!(
+                    return Err(RasterError::Invalid(format!(
                         "Band {band_idx} dimension {spatial_dim:?} has size {actual}, \
                          expected {expected} from top-level spatial_shape"
                     )));
@@ -645,7 +646,7 @@ impl RasterBuilder {
     }
 
     /// Append a null raster.
-    pub fn append_null(&mut self) -> Result<(), ArrowError> {
+    pub fn append_null(&mut self) -> Result<(), RasterError> {
         // Transform: append 6 zeros
         for _ in 0..6 {
             self.transform_values.append_value(0.0);
@@ -673,12 +674,12 @@ impl RasterBuilder {
     }
 
     /// Finish building and return the constructed StructArray.
-    pub fn finish(mut self) -> Result<StructArray, ArrowError> {
+    pub fn finish(mut self) -> Result<StructArray, RasterError> {
         // Build transform list
         let transform_values = self.transform_values.finish();
         let transform_offsets = OffsetBuffer::new(ScalarBuffer::from(self.transform_offsets));
         let DataType::List(transform_field) = RasterSchema::transform_type() else {
-            return Err(ArrowError::SchemaError(
+            return Err(RasterError::Invalid(
                 "Expected list type for transform".to_string(),
             ));
         };
@@ -693,7 +694,7 @@ impl RasterBuilder {
         let spatial_dims_values = self.spatial_dims_values.finish();
         let spatial_dims_offsets = OffsetBuffer::new(ScalarBuffer::from(self.spatial_dims_offsets));
         let DataType::List(spatial_dims_field) = RasterSchema::spatial_dims_type() else {
-            return Err(ArrowError::SchemaError(
+            return Err(RasterError::Invalid(
                 "Expected list type for spatial_dims".to_string(),
             ));
         };
@@ -709,7 +710,7 @@ impl RasterBuilder {
         let spatial_shape_offsets =
             OffsetBuffer::new(ScalarBuffer::from(self.spatial_shape_offsets));
         let DataType::List(spatial_shape_field) = RasterSchema::spatial_shape_type() else {
-            return Err(ArrowError::SchemaError(
+            return Err(RasterError::Invalid(
                 "Expected list type for spatial_shape".to_string(),
             ));
         };
@@ -724,7 +725,7 @@ impl RasterBuilder {
         let dim_names_values = self.band_dim_names_values.finish();
         let dim_names_offsets = OffsetBuffer::new(ScalarBuffer::from(self.band_dim_names_offsets));
         let DataType::List(dim_names_field) = RasterSchema::dim_names_type() else {
-            return Err(ArrowError::SchemaError(
+            return Err(RasterError::Invalid(
                 "Expected list type for dim_names".to_string(),
             ));
         };
@@ -739,7 +740,7 @@ impl RasterBuilder {
         let source_shape_values = self.band_shape_values.finish();
         let source_shape_offsets = OffsetBuffer::new(ScalarBuffer::from(self.band_shape_offsets));
         let DataType::List(source_shape_field) = RasterSchema::source_shape_type() else {
-            return Err(ArrowError::SchemaError(
+            return Err(RasterError::Invalid(
                 "Expected list type for source_shape".to_string(),
             ));
         };
@@ -757,12 +758,12 @@ impl RasterBuilder {
         let view_steps = self.band_view_steps_values.finish();
         let view_offsets = OffsetBuffer::new(ScalarBuffer::from(self.band_view_offsets));
         let DataType::List(view_list_field) = RasterSchema::view_type() else {
-            return Err(ArrowError::SchemaError(
+            return Err(RasterError::Invalid(
                 "Expected list type for view".to_string(),
             ));
         };
         let DataType::Struct(view_struct_fields) = view_list_field.data_type().clone() else {
-            return Err(ArrowError::SchemaError(
+            return Err(RasterError::Invalid(
                 "Expected struct type inside view list".to_string(),
             ));
         };
@@ -792,7 +793,7 @@ impl RasterBuilder {
 
         // Build band struct
         let DataType::Struct(band_fields) = RasterSchema::band_type() else {
-            return Err(ArrowError::SchemaError(
+            return Err(RasterError::Invalid(
                 "Expected struct type for band".to_string(),
             ));
         };
@@ -812,7 +813,7 @@ impl RasterBuilder {
 
         // Build bands list
         let DataType::List(bands_field) = RasterSchema::bands_type() else {
-            return Err(ArrowError::SchemaError(
+            return Err(RasterError::Invalid(
                 "Expected list type for bands".to_string(),
             ));
         };
diff --git a/rust/sedona-raster/src/error.rs b/rust/sedona-raster/src/error.rs
new file mode 100644
index 000000000..4ab383890
--- /dev/null
+++ b/rust/sedona-raster/src/error.rs
@@ -0,0 +1,192 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! The raster crates' domain error type.
+//!
+//! The raster code used to plumb [`arrow_schema::ArrowError`] everywhere,
+//! which forced a `map_err` at every boundary that produced or consumed a
+//! raster error. [`RasterError`] mirrors `sedona-geometry`'s
+//! `SedonaGeometryError`: a small `thiserror` enum that library accessors
+//! return directly, with `From` bridges so callers use `?` instead of a
+//! hand-written `map_err`.
+//!
+//! The one raster-specific addition over the geometry mirror is the
+//! [`RasterError::Arrow`] variant with `#[from] ArrowError`: raster code
+//! calls Arrow APIs constantly, so `?` over an `ArrowError` needs to resolve
+//! to a `RasterError` without a `map_err`.
+//!
+//! Boundaries:
+//! - `From<ArrowError> for RasterError` — `?` over Arrow calls inside the
+//!   raster library.
+//! - `From<RasterError> for DataFusionError` — `?` at UDF boundaries (the
+//!   raster-functions / raster-gdal crates return `DataFusionError`).
+//! - `From<RasterError> for ArrowError` — the bridge back for signatures
+//!   fixed by Arrow, notably `RecordBatchReader::next`.
+//!
+//! Internal-assertion errors ("this is a SedonaDB bug") are intentionally
+//! *not* a variant here: they stay a boundary concern raised through
+//! `sedona_common::sedona_internal_err!`, which produces a `DataFusionError`
+//! directly — same as `sedona-geometry`, which has no internal variant.
+
+use arrow_schema::ArrowError;
+use datafusion_common::DataFusionError;
+use thiserror::Error;
+
+/// Error type for the raster crates.
+///
+/// Mirrors `sedona_geometry::error::SedonaGeometryError`. Construct
+/// [`RasterError::Invalid`] for a bad-input/precondition failure and
+/// [`RasterError::External`] to carry an arbitrary source error; Arrow
+/// errors convert automatically via `?`.
+#[derive(Error, Debug)]
+pub enum RasterError {
+    /// Invalid input or a violated precondition, described by the message.
+    #[error("{0}")]
+    Invalid(String),
+    /// An Arrow error encountered while building or reading raster arrays.
+    #[error(transparent)]
+    Arrow(#[from] ArrowError),
+    /// Any other source error, boxed.
+    #[error(transparent)]
+    External(Box<dyn std::error::Error + Send + Sync>),
+    /// A raster error with no more specific classification.
+    #[error("Unknown raster error")]
+    Unknown,
+}
+
+impl From<RasterError> for DataFusionError {
+    fn from(value: RasterError) -> Self {
+        match value {
+            // Keep Arrow errors typed so DataFusion renders them as Arrow
+            // errors rather than an opaque external error.
+            RasterError::Arrow(e) => DataFusionError::ArrowError(Box::new(e), None),
+            // `Execution` (not `Internal`/`External`) matches the existing
+            // `exec_datafusion_err!` sites this replaces: a user-facing
+            // execution error without the "this is a DataFusion bug" framing.
+            RasterError::Invalid(msg) => DataFusionError::Execution(msg),
+            RasterError::External(e) => DataFusionError::External(e),
+            RasterError::Unknown => DataFusionError::Execution("Unknown raster error".to_string()),
+        }
+    }
+}
+
+impl From<RasterError> for ArrowError {
+    fn from(value: RasterError) -> Self {
+        match value {
+            RasterError::Arrow(e) => e,
+            other => ArrowError::ExternalError(Box::new(other)),
+        }
+    }
+}
+
+impl From<DataFusionError> for RasterError {
+    fn from(value: DataFusionError) -> Self {
+        // Raster accessors lean on `datafusion_common::cast::as_*`, which
+        // return `DataFusionError`. Carry it as a source so `?` works without
+        // a `map_err` and the original message survives round-tripping back to
+        // `DataFusionError` at the UDF boundary.
+        RasterError::External(Box::new(value))
+    }
+}
+
+/// Attach a message to a fallible result and convert it to a [`RasterError`].
+///
+/// Replaces the `map_err(|e| exec_datafusion_err!("context: {e}"))` pattern:
+/// `foo().context("context")?` formats the message as `"context: {source}"`
+/// and, via the `From` bridges above, resolves under `?` in functions that
+/// return `RasterError`, `DataFusionError`, or `ArrowError`.
+pub trait RasterResultExt<T> {
+    /// Prefix the error with an eagerly-evaluated `context` message.
+    fn context(self, context: impl std::fmt::Display) -> Result<T, RasterError>;
+
+    /// Prefix the error with a lazily-computed message, only paid on error.
+    fn with_context<C, F>(self, context: F) -> Result<T, RasterError>
+    where
+        C: std::fmt::Display,
+        F: FnOnce() -> C;
+}
+
+impl<T, E: std::fmt::Display> RasterResultExt<T> for Result<T, E> {
+    fn context(self, context: impl std::fmt::Display) -> Result<T, RasterError> {
+        self.map_err(|e| RasterError::Invalid(format!("{context}: {e}")))
+    }
+
+    fn with_context<C, F>(self, context: F) -> Result<T, RasterError>
+    where
+        C: std::fmt::Display,
+        F: FnOnce() -> C,
+    {
+        self.map_err(|e| RasterError::Invalid(format!("{}: {e}", context())))
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn invalid_and_external_display() {
+        let invalid = RasterError::Invalid("bad band".to_string());
+        assert_eq!(invalid.to_string(), "bad band");
+
+        let source = Box::new(std::io::Error::other("boom"));
+        let external = RasterError::External(source);
+        assert_eq!(external.to_string(), "boom");
+
+        assert_eq!(RasterError::Unknown.to_string(), "Unknown raster error");
+    }
+
+    #[test]
+    fn arrow_round_trips_without_double_wrapping() {
+        // `?` over an ArrowError yields RasterError::Arrow ...
+        let err: RasterError = ArrowError::ComputeError("nope".to_string()).into();
+        assert!(matches!(err, RasterError::Arrow(_)));
+        // ... and the bridge back to ArrowError unwraps it rather than
+        // boxing it as an ExternalError.
+        let back: ArrowError = err.into();
+        assert!(matches!(back, ArrowError::ComputeError(_)));
+    }
+
+    #[test]
+    fn into_datafusion_preserves_kind() {
+        let df: DataFusionError = RasterError::Invalid("bad".to_string()).into();
+        assert!(matches!(df, DataFusionError::Execution(_)));
+
+        let df: DataFusionError = RasterError::Arrow(ArrowError::ComputeError("x".into())).into();
+        assert!(matches!(df, DataFusionError::ArrowError(_, _)));
+    }
+
+    #[test]
+    fn context_prefixes_message() {
+        // Use a plain-Display source so the assertion reads clearly; the point
+        // is that `context` prefixes the message with the source appended.
+        let r: Result<(), RasterError> = Err(RasterError::Invalid("underlying".to_string()));
+        let err = r.context("while doing the thing").unwrap_err();
+        assert_eq!(err.to_string(), "while doing the thing: underlying");
+
+        // Lazy variant is equivalent on the error path.
+        let r: Result<(), RasterError> = Err(RasterError::Invalid("underlying".to_string()));
+        let err = r.with_context(|| "while doing the thing").unwrap_err();
+        assert_eq!(err.to_string(), "while doing the thing: underlying");
+
+        // The source error's own Display is appended verbatim, so an Arrow
+        // source keeps its "Compute error:" prefix.
+        let r: Result<(), ArrowError> = Err(ArrowError::ComputeError("boom".to_string()));
+        let err = r.context("ctx").unwrap_err();
+        assert_eq!(err.to_string(), "ctx: Compute error: boom");
+    }
+}
diff --git a/rust/sedona-raster/src/geo_transform.rs b/rust/sedona-raster/src/geo_transform.rs
index c5cb2ca0f..72a5da27d 100644
--- a/rust/sedona-raster/src/geo_transform.rs
+++ b/rust/sedona-raster/src/geo_transform.rs
@@ -34,7 +34,7 @@
 //!
 //! [`RasterRef::transform`]: crate::traits::RasterRef::transform
 
-use arrow_schema::ArrowError;
+use crate::error::RasterError;
 
 /// An affine geo-transform: six coefficients mapping pixel/line to projection coordinates.
 ///
@@ -59,7 +59,7 @@ pub trait GeoTransformEx {
 
     /// Invert this geo-transform, returning the inverse coefficients for
     /// computing (geo_x, geo_y) -> (x, y) transformations.
-    fn invert(&self) -> Result<GeoTransform, ArrowError>;
+    fn invert(&self) -> Result<GeoTransform, RasterError>;
 
     /// Rotation angle (radians) implied by the coefficients.
     fn rotation(&self) -> f64;
@@ -88,7 +88,7 @@ impl GeoTransformEx for [f64] {
     }
 
     /// Pure-Rust equivalent of GDAL's `GDALInvGeoTransform`.
-    fn invert(&self) -> Result<GeoTransform, ArrowError> {
+    fn invert(&self) -> Result<GeoTransform, RasterError> {
         let gt = self;
 
         // Fast path: no rotation/skew — avoid determinant and precision issues.
@@ -111,7 +111,7 @@ impl GeoTransformEx for [f64] {
             .max(gt[4].abs().max(gt[5].abs()));
 
         if det.abs() <= 1e-10 * magnitude * magnitude {
-            return Err(ArrowError::InvalidArgumentError(
+            return Err(RasterError::Invalid(
                 "Geo transform is uninvertible".to_string(),
             ));
         }
@@ -180,18 +180,18 @@ pub fn geotransform_from_bbox_and_spatial_shape(
     height: u64,
     width: u64,
     registration: Option<&str>,
-) -> Result<GeoTransform, ArrowError> {
+) -> Result<GeoTransform, RasterError> {
     let [xmin, ymin, xmax, ymax] = bbox;
     // Reject a degenerate or inverted bbox: a zero span gives a non-invertible
     // (zero-scale) transform and a negative span isn't north-up.
     if !(xmax > xmin && ymax > ymin) {
-        return Err(ArrowError::InvalidArgumentError(format!(
+        return Err(RasterError::Invalid(format!(
             "bbox must have xmin < xmax and ymin < ymax; got [{xmin}, {ymin}, {xmax}, {ymax}]"
         )));
     }
     // A real raster axis is at least 1 cell.
     if height == 0 || width == 0 {
-        return Err(ArrowError::InvalidArgumentError(
+        return Err(RasterError::Invalid(
             "raster spatial dimensions must be non-zero to derive a transform from a bbox".into(),
         ));
     }
@@ -213,7 +213,7 @@ pub fn geotransform_from_bbox_and_spatial_shape(
         // a cell beyond — the corner sits half a cell outside the bbox.
         "node" => {
             if width < 2.0 || height < 2.0 {
-                return Err(ArrowError::InvalidArgumentError(
+                return Err(RasterError::Invalid(
                     "node-registered grid must be at least 2 cells in each spatial dimension"
                         .into(),
                 ));
@@ -223,7 +223,7 @@ pub fn geotransform_from_bbox_and_spatial_shape(
             (scale_x, scale_y, xmin - scale_x / 2.0, ymax - scale_y / 2.0)
         }
         other => {
-            return Err(ArrowError::InvalidArgumentError(format!(
+            return Err(RasterError::Invalid(format!(
                 "registration must be \"pixel\" or \"node\"; got {other:?}"
             )))
         }
diff --git a/rust/sedona-raster/src/lib.rs b/rust/sedona-raster/src/lib.rs
index 598623ba3..6de897fb7 100644
--- a/rust/sedona-raster/src/lib.rs
+++ b/rust/sedona-raster/src/lib.rs
@@ -19,6 +19,7 @@ pub mod affine_transformation;
 pub mod array;
 pub mod builder;
 pub mod display;
+pub mod error;
 pub mod geo_transform;
 pub mod raster_loader;
 pub mod traits;
diff --git a/rust/sedona-raster/src/traits.rs b/rust/sedona-raster/src/traits.rs
index 055392138..e0143a44d 100644
--- a/rust/sedona-raster/src/traits.rs
+++ b/rust/sedona-raster/src/traits.rs
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use arrow_schema::ArrowError;
+use crate::error::RasterError;
 use sedona_schema::raster::BandDataType;
 
 use crate::builder::{RasterBuilder, StartBandArgs};
@@ -95,9 +95,9 @@ impl<'a> NdBuffer<'a> {
     /// (<https://github.com/apache/sedona-db/issues/899>). Never copies or
     /// allocates — a strided layout returns an error, it is not materialized
     /// here.
-    pub fn as_contiguous(&self) -> Result<&'a [u8], ArrowError> {
+    pub fn as_contiguous(&self) -> Result<&'a [u8], RasterError> {
         if !self.is_contiguous() {
-            return Err(ArrowError::InvalidArgumentError(format!(
+            return Err(RasterError::Invalid(format!(
                 "band view is not contiguous (shape {:?}, strides {:?}); \
                  materialize it with RS_EnsureContiguous before contiguous \
                  byte access — see https://github.com/apache/sedona-db/issues/899",
@@ -110,7 +110,7 @@ impl<'a> NdBuffer<'a> {
         let buffer: &'a [u8] = self.buffer;
         match start.checked_add(len) {
             Some(end) if end <= buffer.len() => Ok(&buffer[start..end]),
-            _ => Err(ArrowError::ExternalError(Box::new(
+            _ => Err(RasterError::External(Box::new(
                 sedona_common::sedona_internal_datafusion_err!(
                     "contiguous region [{start}, {start}+{len}) is out of bounds \
                      for buffer of length {}",
@@ -139,12 +139,12 @@ impl<'a> NdBuffer<'a> {
 ///
 /// This is the shared convention consumers use to recover the source path and
 /// band from a band's `outdb_uri()`.
-pub fn split_outdb_band_fragment(uri: &str) -> Result<(String, u32), ArrowError> {
+pub fn split_outdb_band_fragment(uri: &str) -> Result<(String, u32), RasterError> {
     if let Some((prefix, fragment)) = uri.rsplit_once('#') {
         if let Some(band_str) = fragment.strip_prefix("band=") {
             return match band_str.parse::<u32>() {
                 Ok(band) if band >= 1 => Ok((prefix.to_string(), band)),
-                _ => Err(ArrowError::InvalidArgumentError(format!(
+                _ => Err(RasterError::Invalid(format!(
                     "Invalid band index in outdb URI fragment '#band={band_str}': expected a positive integer in 1..=u32::MAX"
                 ))),
             };
@@ -162,12 +162,12 @@ pub trait RasterRef {
     /// Number of bands/variables
     fn num_bands(&self) -> usize;
 
-    /// Access a band by 0-based index. Returns an `ArrowError` when the
+    /// Access a band by 0-based index. Returns a `RasterError` when the
     /// index is out of range or when the underlying schema is malformed
     /// (unknown data-type discriminant, corrupt view, etc.). The latter
     /// cases route through `sedona_common::sedona_internal_datafusion_err!`
     /// so they carry the standardised "SedonaDB internal error" framing.
-    fn band(&self, index: usize) -> Result<Box<dyn BandRef + '_>, ArrowError>;
+    fn band(&self, index: usize) -> Result<Box<dyn BandRef + '_>, RasterError>;
 
     /// Band name (e.g., Zarr variable name). None for unnamed bands.
     fn band_name(&self, index: usize) -> Option<&str>;
@@ -245,21 +245,19 @@ pub trait RasterRef {
     /// Width in pixels — size of the X spatial dimension from the top-level
     /// `spatial_shape`. Errors if `spatial_shape` is empty, which is an
     /// invariant violation rather than a legitimate "no value" state.
-    fn width(&self) -> Result<i64, ArrowError> {
+    fn width(&self) -> Result<i64, RasterError> {
         let shape = self.spatial_shape();
         shape.first().copied().ok_or_else(|| {
-            ArrowError::InvalidArgumentError(
-                "raster has no width (spatial_shape is empty)".to_string(),
-            )
+            RasterError::Invalid("raster has no width (spatial_shape is empty)".to_string())
         })
     }
 
     /// Height in pixels — size of the Y spatial dimension from the top-level
     /// `spatial_shape`. Errors if `spatial_shape` has fewer than two entries.
-    fn height(&self) -> Result<i64, ArrowError> {
+    fn height(&self) -> Result<i64, RasterError> {
         let shape = self.spatial_shape();
         shape.get(1).copied().ok_or_else(|| {
-            ArrowError::InvalidArgumentError(format!(
+            RasterError::Invalid(format!(
                 "raster has no height (spatial_shape has {} entries, need >= 2)",
                 shape.len()
             ))
@@ -268,12 +266,10 @@ pub trait RasterRef {
 
     /// Look up a band by name. Returns an error if no band has that
     /// name or if the matching band is malformed.
-    fn band_by_name(&self, name: &str) -> Result<Box<dyn BandRef + '_>, ArrowError> {
+    fn band_by_name(&self, name: &str) -> Result<Box<dyn BandRef + '_>, RasterError> {
         let i = (0..self.num_bands())
             .find(|&i| self.band_name(i) == Some(name))
-            .ok_or_else(|| {
-                ArrowError::InvalidArgumentError(format!("Band with name '{name}' not found"))
-            })?;
+            .ok_or_else(|| RasterError::Invalid(format!("Band with name '{name}' not found")))?;
         self.band(i)
     }
 }
@@ -416,7 +412,7 @@ pub trait BandRef {
     /// `offset` are computed by composing the view with the source's
     /// natural C-order byte strides. Strides may be zero (broadcast) or
     /// negative (reverse iteration).
-    fn nd_buffer(&self) -> Result<NdBuffer<'_>, ArrowError>;
+    fn nd_buffer(&self) -> Result<NdBuffer<'_>, RasterError>;
 
     /// Nodata value interpreted as f64.
     ///
@@ -431,7 +427,7 @@ pub trait BandRef {
     /// value. Use `nodata()` directly to recover the exact bytes when full
     /// integer precision matters (e.g. when nodata is the type's extreme
     /// value like `0xFF…FF`).
-    fn nodata_as_f64(&self) -> Result<Option<f64>, ArrowError> {
+    fn nodata_as_f64(&self) -> Result<Option<f64>, RasterError> {
         let bytes = match self.nodata() {
             Some(b) => b,
             None => return Ok(None),
@@ -465,7 +461,7 @@ pub trait BandRef {
         &self,
         builder: &mut RasterBuilder,
         overrides: BandOverrides<'_>,
-    ) -> Result<(), ArrowError> {
+    ) -> Result<(), RasterError> {
         let inherited_dims = self.dim_names();
         let dim_names: Vec<&str> = match overrides.dim_names {
             Some(d) => d.to_vec(),
@@ -499,7 +495,7 @@ pub trait BandRef {
     /// [`RasterBuilder::append_band_data_from`]), keeping the buffer plumbing
     /// encapsulated rather than exposing a raw `Buffer` accessor. Call after the
     /// band's schema has been written (e.g. by [`Self::copy_into`]).
-    fn append_data_into(&self, builder: &mut RasterBuilder) -> Result<(), ArrowError> {
+    fn append_data_into(&self, builder: &mut RasterBuilder) -> Result<(), RasterError> {
         if self.is_indb() {
             let ndb = self.nd_buffer()?;
             builder.band_data_writer().append_value(ndb.buffer);
@@ -515,11 +511,11 @@ pub trait BandRef {
 /// The bytes are expected to be in little-endian order and exactly match the
 /// byte size of the data type. Internal helper for the lossless wrapper;
 /// non-i64/u64 callers reach for `nodata_bytes_to_f64_lossless` instead.
-fn nodata_bytes_to_f64(bytes: &[u8], dt: &BandDataType) -> Result<f64, ArrowError> {
+fn nodata_bytes_to_f64(bytes: &[u8], dt: &BandDataType) -> Result<f64, RasterError> {
     macro_rules! read_le {
         ($t:ty, $n:expr) => {{
             let arr: [u8; $n] = bytes.try_into().map_err(|_| {
-                ArrowError::InvalidArgumentError(format!(
+                RasterError::Invalid(format!(
                     "Invalid nodata byte length for {:?}: expected {}, got {}",
                     dt,
                     $n,
@@ -533,7 +529,7 @@ fn nodata_bytes_to_f64(bytes: &[u8], dt: &BandDataType) -> Result<f64, ArrowErro
     match dt {
         BandDataType::UInt8 => {
             if bytes.len() != 1 {
-                return Err(ArrowError::InvalidArgumentError(format!(
+                return Err(RasterError::Invalid(format!(
                     "Invalid nodata byte length for UInt8: expected 1, got {}",
                     bytes.len()
                 )));
@@ -542,7 +538,7 @@ fn nodata_bytes_to_f64(bytes: &[u8], dt: &BandDataType) -> Result<f64, ArrowErro
         }
         BandDataType::Int8 => {
             if bytes.len() != 1 {
-                return Err(ArrowError::InvalidArgumentError(format!(
+                return Err(RasterError::Invalid(format!(
                     "Invalid nodata byte length for Int8: expected 1, got {}",
                     bytes.len()
                 )));
@@ -568,18 +564,18 @@ fn nodata_bytes_to_f64(bytes: &[u8], dt: &BandDataType) -> Result<f64, ArrowErro
 /// pixel == nodata) should prefer this over the lossy variant — a rounded
 /// `0xFFFF_FFFF_FFFF_FFFE` sentinel can silently collide with a real
 /// pixel value.
-pub fn nodata_bytes_to_f64_lossless(bytes: &[u8], dt: &BandDataType) -> Result<f64, ArrowError> {
+pub fn nodata_bytes_to_f64_lossless(bytes: &[u8], dt: &BandDataType) -> Result<f64, RasterError> {
     match dt {
         BandDataType::UInt64 => {
             let arr: [u8; 8] = bytes.try_into().map_err(|_| {
-                ArrowError::InvalidArgumentError(format!(
+                RasterError::Invalid(format!(
                     "Invalid nodata byte length for UInt64: expected 8, got {}",
                     bytes.len()
                 ))
             })?;
             let v = u64::from_le_bytes(arr);
             if v > (1u64 << 53) {
-                return Err(ArrowError::InvalidArgumentError(format!(
+                return Err(RasterError::Invalid(format!(
                     "UInt64 nodata value {v} cannot be represented exactly as f64 \
                      (magnitude > 2^53); use the raw nodata bytes instead"
                 )));
@@ -588,14 +584,14 @@ pub fn nodata_bytes_to_f64_lossless(bytes: &[u8], dt: &BandDataType) -> Result<f
         }
         BandDataType::Int64 => {
             let arr: [u8; 8] = bytes.try_into().map_err(|_| {
-                ArrowError::InvalidArgumentError(format!(
+                RasterError::Invalid(format!(
                     "Invalid nodata byte length for Int64: expected 8, got {}",
                     bytes.len()
                 ))
             })?;
             let v = i64::from_le_bytes(arr);
             if v.unsigned_abs() > (1u64 << 53) {
-                return Err(ArrowError::InvalidArgumentError(format!(
+                return Err(RasterError::Invalid(format!(
                     "Int64 nodata value {v} cannot be represented exactly as f64 \
                      (magnitude > 2^53); use the raw nodata bytes instead"
                 )));
@@ -614,10 +610,10 @@ pub fn nodata_bytes_to_f64_lossless(bytes: &[u8], dt: &BandDataType) -> Result<f
 /// integer beyond 2^53 (which can't have arrived losslessly through `f64`).
 /// `Float32` is the one lossy case allowed — it rounds to the nearest `f32`, as
 /// any f64 → f32 narrowing does.
-pub fn nodata_f64_to_bytes(value: f64, dt: &BandDataType) -> Result<Vec<u8>, ArrowError> {
-    fn check_integer(value: f64, min: f64, max: f64, dt: &BandDataType) -> Result<(), ArrowError> {
+pub fn nodata_f64_to_bytes(value: f64, dt: &BandDataType) -> Result<Vec<u8>, RasterError> {
+    fn check_integer(value: f64, min: f64, max: f64, dt: &BandDataType) -> Result<(), RasterError> {
         if value.fract() != 0.0 || value < min || value > max {
-            return Err(ArrowError::InvalidArgumentError(format!(
+            return Err(RasterError::Invalid(format!(
                 "nodata value {value} is not a valid {dt:?} value"
             )));
         }
@@ -847,7 +843,7 @@ mod tests {
         fn nodata(&self) -> Option<&[u8]> {
             None
         }
-        fn nd_buffer(&self) -> Result<NdBuffer<'_>, ArrowError> {
+        fn nd_buffer(&self) -> Result<NdBuffer<'_>, RasterError> {
             unimplemented!("not used in is_spatial_2d tests")
         }
         fn is_indb(&self) -> bool {
diff --git a/rust/sedona-raster/src/view_entries.rs b/rust/sedona-raster/src/view_entries.rs
index 3e8f767d5..63d4d6681 100644
--- a/rust/sedona-raster/src/view_entries.rs
+++ b/rust/sedona-raster/src/view_entries.rs
@@ -20,7 +20,7 @@
 //! and is the entry point for all view-machinery operations
 //! (validation, identity check, visible-shape derivation, composition).
 
-use arrow_schema::ArrowError;
+use crate::error::RasterError;
 
 /// One per-dimension entry of a band's logical view. Describes how a
 /// visible axis maps onto an axis of the underlying source buffer.
@@ -65,7 +65,7 @@ impl ViewEntries {
     /// Wrap a pre-built vector of entries and validate it against
     /// `source_shape` in one step — [`Self::new`] followed by
     /// [`Self::validate`].
-    pub fn try_new(inner: Vec<ViewEntry>, source_shape: &[i64]) -> Result<Self, ArrowError> {
+    pub fn try_new(inner: Vec<ViewEntry>, source_shape: &[i64]) -> Result<Self, RasterError> {
         let entries = Self::new(inner);
         entries.validate(source_shape)?;
         Ok(entries)
@@ -141,10 +141,10 @@ impl ViewEntries {
     /// - When `steps > 0`: `start ∈ [0, source_shape[source_axis])`,
     ///   and when `step != 0` the last addressed element
     ///   `start + (steps - 1) * step` is also in that range.
-    pub fn validate(&self, source_shape: &[i64]) -> Result<(), ArrowError> {
+    pub fn validate(&self, source_shape: &[i64]) -> Result<(), RasterError> {
         let ndim = source_shape.len();
         if self.0.len() != ndim {
-            return Err(ArrowError::InvalidArgumentError(format!(
+            return Err(RasterError::Invalid(format!(
                 "view length ({}) must equal source_shape length ({ndim})",
                 self.0.len()
             )));
@@ -152,14 +152,14 @@ impl ViewEntries {
         let mut seen = vec![false; ndim];
         for (k, v) in self.0.iter().enumerate() {
             if v.source_axis < 0 || (v.source_axis as usize) >= ndim {
-                return Err(ArrowError::InvalidArgumentError(format!(
+                return Err(RasterError::Invalid(format!(
                     "view[{k}].source_axis = {} is out of range [0, {ndim})",
                     v.source_axis
                 )));
             }
             let sa = v.source_axis as usize;
             if seen[sa] {
-                return Err(ArrowError::InvalidArgumentError(format!(
+                return Err(RasterError::Invalid(format!(
                     "view source_axis values must be a permutation of 0..{ndim}; \
                      axis {sa} appears more than once"
                 )));
@@ -167,13 +167,13 @@ impl ViewEntries {
             seen[sa] = true;
 
             if v.steps < 0 {
-                return Err(ArrowError::InvalidArgumentError(format!(
+                return Err(RasterError::Invalid(format!(
                     "view[{k}].steps = {} must be >= 0",
                     v.steps
                 )));
             }
             if source_shape[sa] < 0 {
-                return Err(ArrowError::InvalidArgumentError(format!(
+                return Err(RasterError::Invalid(format!(
                     "source_shape[{sa}] = {} must be >= 0",
                     source_shape[sa]
                 )));
@@ -181,7 +181,7 @@ impl ViewEntries {
             if v.steps > 0 {
                 let s = source_shape[sa];
                 if v.start < 0 || v.start >= s {
-                    return Err(ArrowError::InvalidArgumentError(format!(
+                    return Err(RasterError::Invalid(format!(
                         "view[{k}].start = {} is out of range [0, {s}) for source axis {sa}",
                         v.start
                     )));
@@ -195,14 +195,14 @@ impl ViewEntries {
                         .checked_mul(v.step)
                         .and_then(|d| v.start.checked_add(d))
                         .ok_or_else(|| {
-                            ArrowError::InvalidArgumentError(format!(
+                            RasterError::Invalid(format!(
                                 "view[{k}] last-element index overflows i64 for \
                                  start={}, step={}, steps={} on source axis {sa}",
                                 v.start, v.step, v.steps
                             ))
                         })?;
                     if last < 0 || last >= s {
-                        return Err(ArrowError::InvalidArgumentError(format!(
+                        return Err(RasterError::Invalid(format!(
                             "view[{k}] addresses element {last} which is out of range \
                              [0, {s}) for source axis {sa}"
                         )));
@@ -234,11 +234,11 @@ impl ViewEntries {
     /// [`validate`] the result against the source shape before use.
     ///
     /// [`validate`]: Self::validate
-    pub fn compose(&self, next: &Self) -> Result<Self, ArrowError> {
+    pub fn compose(&self, next: &Self) -> Result<Self, RasterError> {
         let mut out = Vec::with_capacity(next.0.len());
         for (k, next_entry) in next.0.iter().enumerate() {
             if next_entry.source_axis < 0 || (next_entry.source_axis as usize) >= self.0.len() {
-                return Err(ArrowError::InvalidArgumentError(format!(
+                return Err(RasterError::Invalid(format!(
                     "compose: next[{k}].source_axis ({}) is out of range \
                      for input with {} visible axes",
                     next_entry.source_axis,
@@ -247,21 +247,21 @@ impl ViewEntries {
             }
             let input = &self.0[next_entry.source_axis as usize];
             let step = next_entry.step.checked_mul(input.step).ok_or_else(|| {
-                ArrowError::InvalidArgumentError(format!(
+                RasterError::Invalid(format!(
                     "compose: step product overflows i64 at axis {k} \
                      (next.step={}, input.step={})",
                     next_entry.step, input.step
                 ))
             })?;
             let start_offset = next_entry.start.checked_mul(input.step).ok_or_else(|| {
-                ArrowError::InvalidArgumentError(format!(
+                RasterError::Invalid(format!(
                     "compose: next.start * input.step overflows i64 at axis {k} \
                      (next.start={}, input.step={})",
                     next_entry.start, input.step
                 ))
             })?;
             let start = input.start.checked_add(start_offset).ok_or_else(|| {
-                ArrowError::InvalidArgumentError(format!(
+                RasterError::Invalid(format!(
                     "compose: composed start overflows i64 at axis {k} \
                      (input.start={}, offset={})",
                     input.start, start_offset